PlanetScale → Neon
Move a PlanetScale Vitess MySQL database to Neon Postgres with pgloader. Pause writes first. pgloader is a point-in-time copy. Use a direct Neon connection, put the endpoint ID in the password field, and check every type cast before you point the app at Neon.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
PlanetScale
- Branching, deploy requests, and Metal performance make Vitess MySQL or managed Postgres appealing for production apps
- A mature data model and query ecosystem supports serious production workloads
- Engine choice, Vitess limits, and paid-plan economics change how you dump, branch, and cut over
- Schema design, migration, tuning, and operations require specialist attention
Neon
- Serverless PostgreSQL with branching and scale-to-zero creates excellent developer workflows
- A mature data model and query ecosystem supports serious production workloads
- Platform-specific branching, compute behavior, and pricing require workload-aware design
- Schema design, migration, tuning, and operations require specialist attention
Neon: Serverless PostgreSQL with branching and scale-to-zero creates excellent developer workflows. This removes a major source-side concern: Engine choice, Vitess limits, and paid-plan economics change how you dump, branch, and cut over.
What you lose: Branching, deploy requests, and Metal performance make Vitess MySQL or managed Postgres appealing for production apps. What you inherit: Platform-specific branching, compute behavior, and pricing require workload-aware design.
Jump to a section
Know the shape of the move.
This timeline assumes
- The source is PlanetScale Vitess (MySQL-compatible), not PlanetScale Postgres. A same-engine Postgres move is a different playbook.
- Neon documents a 1 GB Free plan cap. Larger data needs a paid Neon plan before you load.
- You can pause application writes. Neon says pgloader will not catch changes after it starts.
- The machine running pgloader is allowed through PlanetScale IP restrictions and Neon IP allow rules.
- We keep a pscale dump or logical backup until Neon has passed one application cycle.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Schemas and tables | partial | critical | pgloader creates Postgres tables and can put them in a schema named after the MySQL database. That is not always public. | Name the Neon database on purpose and rename the schema to public if that is what the app expects. |
| Row data | partial | critical | pgloader copies rows in one point-in-time run. Writes after start stay on PlanetScale. | Freeze writes, then compare counts and a recent-row sample on both sides. |
| Types | partial | critical | Neon says AUTO_INCREMENT becomes SERIAL, TINYINT becomes BOOLEAN, and DATETIME becomes TIMESTAMP. Other casts follow pgloader MySQL rules unless you override CAST. | Dump the source schema, review every mapped type, and add CAST lines for columns you cannot accept as defaults. |
| Indexes, keys, and sequences | partial | critical | pgloader reports index, primary key, foreign key, and sequence reset work. Failures show up as errors in that summary, not as a silent Postgres app. | Read the pgloader summary. Query sequences and foreign keys before you open writes. |
| PlanetScale branching and deploy requests | lost | high | Vitess branches and deploy requests do not become Neon branches. | Create a Neon branch from the migrated primary if you still want branch-per-PR, and stop treating PlanetScale deploy requests as history. |
| Vitess sharding and connection behavior | lost | high | Vitess limits, no stored procedures, and connection pooling behavior stay behind. | Retest queries that depended on Vitess or on PlanetScale's pooler. |
| Stored routines and views | manual | high | PlanetScale Vitess does not give you portable stored procedures. Views may land in an unexpected schema. | Recreate views in public and rewrite routines as application SQL or Postgres functions. |
| Users, roles, and IP allow lists | manual | critical | MySQL users do not become Neon roles. Neon needs its own roles and allow rules. | Create least-privilege Neon roles and allow the app and the pgloader host only. |
| Connection strings and pooling | manual | critical | Neon tells you to disable the pooling toggle for pgloader, embed endpoint=ENDPOINT_ID in the password, and drop channel_binding. The app later uses a Neon pooled string that is not the pgloader string. | Keep two strings. Use direct for pgloader. Use the pooled string only after you test the driver. |
| ORM SQL dialect | manual | critical | Prisma, Drizzle, or Rails SQL written for MySQL will not run unchanged on Postgres. | Change the provider, generate migrations against Neon, and run the app test suite before DNS or secret cutover. |
| pgloader driver bugs | manual | high | Neon documents a mysql 255 fell through ECASE error and points at building pgloader from source. | Rehearse on a copy. If you hit ECASE, use the documented source build before you touch production. |
| PlanetScale query insights and backups | lost | medium | Insights history and Vitess backup windows stay in PlanetScale. | Take a pscale dump first and configure Neon backups before you send traffic. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| PlanetScale database | Neon project database | manual | Create the Neon database name before pgloader runs. |
| MySQL schema or database name | Postgres schema, often renamed to public | transform | Do not leave app search_path pointing at the old name by accident. |
| AUTO_INCREMENT | SERIAL or identity | transform | Reset sequences after load. |
| TINYINT | BOOLEAN unless you CAST otherwise | transform | Check columns that stored 0/1 as integers you still query as numbers. |
| DATETIME | TIMESTAMP | transform | Confirm time zone handling in the app. |
| Primary and foreign keys | Postgres constraints | transform | Read the pgloader foreign-key line. |
| Direct PlanetScale MySQL URL | pgloader source with sslmode=require | transform | Neon asks for the Direct connection string, not a pooled one. |
| Neon console URL | pgloader destination with endpoint ID in the password | transform | Remove channel_binding. |
| Application DATABASE_URL | Neon URL after verification | manual | Switch only after counts and app tests pass. |
Make the move recoverable.
Create the source-of-truth backup
Keep a Vitess dump you can restore even if pgloader or Neon fails.
- Record table counts, sizes, and the PlanetScale engine (Vitess, not Postgres).
- Run pscale database dump against the production branch into an immutable directory.
- Export the schema separately so type review does not depend on a live session.
- Confirm the pgloader host is allowed on both IP lists.
- Create the Neon project and database on a plan that fits the data size.
Proof to capture: A dated manifest lists the dump path, hashes, table counts, and the Neon plan limit.
pgloader config
Give pgloader a direct, TLS, endpoint-aware destination.
- Use the PlanetScale Direct MySQL string with sslmode=require.
- Disable Neon pooling and add endpoint=ID to the password.
- Drop channel_binding. Add CAST rules you already approved.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
App dialect change
Stop shipping MySQL SQL to Postgres.
- Point a branch at Neon.
- Change the provider and regenerate migrations if you use an ORM.
- Run queries that used MySQL-only functions.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
Writes continue during pgloader
The app still writes to PlanetScale after pgloader starts.
- Consequence
- Those rows never reach Neon.
- Mitigation
- Pause writers, then start pgloader, then reconcile counts.
Stop if: Any production write lands on PlanetScale after the freeze.
TINYINT becomes BOOLEAN
A column you treated as an integer is now true/false.
- Consequence
- Queries and ORMs return the wrong type.
- Mitigation
- Review every TINYINT and CAST the ones that are not booleans.
Stop if: A critical column changed meaning in the pilot.
Pooled Neon URL used for pgloader
pgloader uses the pooled host or keeps channel_binding.
- Consequence
- The load fails or stalls.
- Mitigation
- Use the direct string and the endpoint-password workaround Neon documents.
Stop if: pgloader cannot complete a rehearsal load.
Do the work in this order.
- Days 1–3 · inventory
Inventory and types
6–12 hours active2–3 days elapsedSchema review waiting- Confirm Vitess, not PlanetScale Postgres.
- List tables, types, and MySQL-only SQL.
- Pick CAST overrides.
Depends on: PlanetScale and Neon admin access
Stop / go checkpointDump?
Go when: Engine, size, and type map are signed.
Stop when: The source is PlanetScale Postgres or the Neon plan is too small.
- Days 3–5 · backup
Dump and allow lists
4–8 hours active1–3 days elapsedDump and IP rules waiting- Take a pscale dump.
- Allow the pgloader host on both sides.
- Create the Neon database.
Depends on: Approved inventory
Stop / go checkpointPilot?
Go when: The dump hashes and the Neon project exist.
Stop when: The dump is incomplete.
- Days 5–12 · pilot
pgloader rehearsal
10–28 hours active4–8 days elapsedpgloader runtime waiting- Run pgloader against a Neon branch.
- Fix schema name, types, and sequences.
- Run the app test suite.
Depends on: Verified dump
Stop / go checkpointCut over?
Go when: Counts, types, and app tests pass.
Stop when: A critical type or query is wrong.
- Days 12–16 · cutover
Freeze and copy
6–16 hours active1–3 days elapsedWrite freeze waiting- Pause PlanetScale writes.
- Run production pgloader.
- Switch the app URL after reconciliation.
Depends on: Passed pilot
Stop / go checkpointOpen writes?
Go when: Neon is the only writer and counts match.
Stop when: A row is missing or a writer is still on PlanetScale.
- Days 15–20 · observe
Observe and close
4–6 hours active4–6 days elapsedApplication cycle waiting- Watch errors, latency, and sequences.
- Keep the PlanetScale dump.
Depends on: Verified cutover
Stop / go checkpointClose rollback?
Go when: No trigger fires through one cycle.
Stop when: Data or auth queries regress.
Cut over with a way back.
Cutover
Make Neon the only writable database after a frozen pgloader copy.
- Pause application and job writes to PlanetScale.
- Take a final pscale dump.
- Run the approved pgloader config against production Neon.
- Reconcile counts, sequences, and a recent-row sample.
- Switch DATABASE_URL and keep PlanetScale read-only.
Proof to capture: Neon serves the app. Counts match. No writer is still on PlanetScale.
Rollback
Return writes to PlanetScale without losing Neon-era rows you still want.
- Stop writes to Neon.
- Point the app back at PlanetScale.
- Export Neon rows written after cutover.
- Apply only reviewed rows to PlanetScale.
- Run the same app checks on PlanetScale.
Proof to capture: PlanetScale is writable again and Neon is not receiving production writes.
- Count mismatch
- Type or sequence error
- App query failure
- Write landed on the wrong host
- pgloader did not finish
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | Vitess source | Confirm the PlanetScale engine is Vitess/MySQL before you run pgloader. | Source is MySQL-compatible Vitess. | Console screenshot. | |
V-02Blocking | Row counts | Compare table counts on PlanetScale and Neon after the freeze. | Every scoped table matches or has a signed exception. | Count sheet. | |
V-03Blocking | Cast review | Diff source schema against Neon information_schema for TINYINT, DATETIME, and AUTO_INCREMENT columns. | Every critical column has the approved type. | Type ledger. | |
V-04Blocking | Sequences and foreign keys | Insert a row and inspect pgloader's key summary. | New IDs continue and FKs hold. | pgloader summary and insert log. | |
V-05Blocking | ORM and queries | Run the application test suite against Neon. | No MySQL-only SQL remains on a critical path. | Test report. | |
V-06Blocking | Direct versus pooled URLs | Confirm pgloader used the direct endpoint-password URL and the app uses the intended Neon URL. | Two documented strings, no leftover PlanetScale URL in production. | Config audit. | |
V-07Blocking | Roles and IP allow | Connect as the app role and as a denied IP. | Least privilege and denied IPs fail. | Access test. | |
V-08Blocking | Dump restore | Prove the pscale dump exists and the PlanetScale branch still accepts a rollback URL. | You can point the app back. | Dump path and rollback note. |
Remove the scaffolding safely.
Safe after: One application cycle, Neon backups configured, and seven stable days.
- Archive the pscale dump and the pgloader log.
- Revoke the PlanetScale password used by pgloader.
- Remove the pgloader host from allow lists you no longer need.
- Keep PlanetScale until billing and rollback close.
- Document the Neon direct and pooled URLs.
- Schedule the next Neon backup and role review.
When the plan met reality.
First-hand accounts are preferred. Vendor case studies are labeled, and every note below is an editorial paraphrase—follow the link for the full context.
Ishan Anand wrote a pgloader walkthrough after PlanetScale ended its free tier. He treats Neon as a serverless Postgres alternative, not a MySQL clone, and streams schema plus rows with a .load file. The Neon password must include endpoint=ID. He then checks tables in the Neon SQL editor. He notes leftover MySQL-only features still need app changes after the load.
- Put the Neon endpoint ID in the password field and drop the pooled host before you run pgloader.
- Compare row counts and a few application queries after the load. Do not stop at a green pgloader summary.
- The Hobby-plan shutdown, not a feature gap, is what sent many small apps to Neon.
- pgloader moves the relational shape. MySQL-only SQL still has to be rewritten.
Koko moved Prisma MySQL apps off PlanetScale Hobby onto Neon. Prisma db push created the Postgres schema, including a schema=DB_NAME query param so tables did not land in public. pgloader then copied data only through a pscale connect tunnel, because a direct aws.connect.psdb.cloud URL failed without TLS. The Neon URL needed endpoint=ID in the password, no -pooler host, and quote identifiers for camelCase columns.
- Push the Prisma Postgres schema first, then run pgloader with data only and quote identifiers.
- Use pscale connect as a local MySQL tunnel if pgloader cannot satisfy PlanetScale TLS on the public host.
- A raw pgloader URL against PlanetScale often fails on TLS before any rows move.
- Prisma schema= and Neon endpoint= both have to be right, or you load the wrong schema on the wrong compute.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Neon: migrate from PlanetScale MySQL with pgloaderAccessed 2026-10-29
- PlanetScale: pscale database dump (Vitess)Accessed 2026-10-29
- PostgreSQL: SQL dumpAccessed 2026-10-29