PlanetScale → PostgreSQL
Move a PlanetScale Vitess MySQL database to PostgreSQL with a dump plus pgloader, or a dump and a tool you already tested. Pause writes first. Vitess branches, deploy requests, and stored routines do not come along. TINYINT and DATETIME need a cast review.
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
PostgreSQL
- Powerful SQL, extensions, integrity, and open governance support demanding workloads
- A mature data model and query ecosystem supports serious production workloads
- Tuning, upgrades, and sophisticated features require experienced operators
- Schema design, migration, tuning, and operations require specialist attention
PostgreSQL: Powerful SQL, extensions, integrity, and open governance support demanding workloads. 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: Tuning, upgrades, and sophisticated features require experienced operators.
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.
- You can pause application writes for the copy. pgloader will not catch changes after it starts.
- The machine running the copy is allowed through PlanetScale IP rules and the destination firewall.
- You keep a pscale dump until PostgreSQL has passed one application cycle.
- The app can change from MySQL SQL to PostgreSQL SQL before you point production at the new host.
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 instead of public. | Name the destination database on purpose. Rename the schema to public if that is what the app expects. |
| Row data | partial | critical | A dump or pgloader run is a point-in-time copy. Writes after start stay on PlanetScale. | Freeze writes, then compare counts and a recent-row sample. |
| Types | partial | critical | pgloader maps AUTO_INCREMENT toward serial or identity, TINYINT toward boolean, and DATETIME toward timestamp unless you override CAST. | Dump the source schema. 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 in that summary. | Read the summary. Insert a row and check the next id. |
| Stored routines | lost | high | PlanetScale Vitess does not give you portable stored procedures, functions, triggers, or events. | Rewrite needed logic in the app or as Postgres functions before cutover. |
| PlanetScale branching and deploy requests | lost | high | Vitess branches do not become Postgres branches. | Create destination branches yourself if you still want branch-per-PR. |
| Vitess connection and sharding behavior | lost | high | Vitess limits and PlanetScale pooling stay behind. | Retest queries that depended on Vitess or the PlanetScale pooler. |
| Users and IP allow lists | manual | critical | MySQL users do not become Postgres roles. | Create least-privilege roles and allow only the app and the copy host. |
| ORM SQL dialect | manual | critical | Prisma, Drizzle, or Rails SQL written for MySQL will not run unchanged. | Change the provider, generate migrations against Postgres, and run the test suite. |
| Query insights and backup windows | lost | medium | PlanetScale insights and Vitess backup history stay in PlanetScale. | Take a pscale dump first. Configure destination backups before traffic. |
| Foreign keys skipped by some loaders | partial | high | Some Vitess or import paths skip foreign keys. pgloader can create them and still fail a subset. | Query pg_constraint after load. Add missing keys before you open writes. |
| Edge or HTTP proxies | manual | high | Apps that talked to PlanetScale over HTTP from an edge runtime need a new path to Postgres. | Test the production runtime, not only a laptop connection. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| PlanetScale database | PostgreSQL database | manual | Create the destination name before the load. |
| MySQL schema name | Postgres schema, often renamed to public | transform | Do not leave search_path pointing at the old name. |
| AUTO_INCREMENT | serial or identity | transform | Reset sequences after load. |
| TINYINT | boolean unless you CAST otherwise | transform | Check columns you queried as integers. |
| DATETIME | timestamp | transform | Confirm time zone handling. |
| Primary and foreign keys | Postgres constraints | transform | Read the loader summary. |
| Direct PlanetScale MySQL URL | pgloader source with TLS | transform | Use the direct connection, not a pooled one. |
| Application DATABASE_URL | Postgres URL after verification | manual | Switch only after counts and app tests pass. |
| Vitess branch or deploy request | No destination | unsupported | Recreate destination branches if you still want them. |
Make the move recoverable.
Create the source-of-truth backup
Keep a Vitess dump you can restore even if pgloader or the new host fails.
- Confirm the engine is Vitess, not PlanetScale Postgres.
- Record table counts and sizes.
- Run pscale database dump against the production branch.
- Export the schema separately for type review.
- Create the destination database on a host that fits the size.
Proof to capture: A dated manifest with dump path, hashes, table counts, and destination plan limits.
pgloader config
Copy MySQL tables into PostgreSQL with explicit casts.
- Use the PlanetScale direct MySQL string with sslmode=require.
- Point at a Postgres URL the loader can reach.
- Rename the schema to public if the app expects that.
- 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 the new database.
- Change the provider and regenerate migrations.
- 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 the copy
The app still writes to PlanetScale after pgloader starts.
- Consequence
- Those rows never reach Postgres.
- Mitigation
- Pause writers, then copy, 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 or false.
- Consequence
- Queries 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.
You run pgloader against PlanetScale Postgres
The source is already Postgres.
- Consequence
- The wrong tool mangles a same-engine move.
- Mitigation
- Confirm Vitess in the console first.
Stop if: The source is PlanetScale Postgres.
Edge runtime cannot open a Postgres socket
The old app used PlanetScale's HTTP driver from an edge function.
- Consequence
- Production cannot query the new database.
- Mitigation
- Test the real runtime.
Stop if: The production runtime cannot complete a rehearsal query.
Do the work in this order.
- Days 1–3 · inventory
Inventory and types
6–12 hours active2–3 days elapsedSchema review waiting- Confirm Vitess.
- List tables, types, and MySQL-only SQL.
- Pick CAST overrides.
Stop / go checkpointDump?
Go when: Engine, size, and type map are signed.
Stop when: The source is PlanetScale Postgres.
- 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 copy host.
- Create the Postgres database.
Depends on: inventory
Stop / go checkpointPilot?
Go when: The dump hashes and the destination exist.
Stop when: The dump is incomplete.
- Days 5–12 · pilot
pgloader rehearsal
10–28 hours active4–8 days elapsedLoader runtime waiting- Run pgloader against a copy.
- Fix schema name, types, and sequences.
- Run the app test suite.
Depends on: backup
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: pilot
Stop / go checkpointOpen writes?
Go when: Postgres 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: 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 PostgreSQL the only writable database after a frozen copy.
- Pause application and job writes to PlanetScale.
- Take a final pscale dump.
- Run the approved pgloader config.
- Reconcile counts, sequences, and a recent-row sample.
- Switch DATABASE_URL and keep PlanetScale read-only.
Proof to capture: Postgres serves the app. Counts match. No writer is still on PlanetScale.
Rollback
Return writes to PlanetScale without losing Postgres-era rows you still want.
- Stop writes to Postgres.
- Point the app back at PlanetScale.
- Export Postgres 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 Postgres is not receiving production writes.
- Count mismatch
- Type or sequence error
- App query failure
- Write landed on the wrong host
- The loader 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. | Source is MySQL-compatible Vitess. | Console screenshot. | |
V-02Blocking | Row counts | Compare table counts after the freeze. | Every scoped table matches or has a signed exception. | Count sheet. | |
V-03Blocking | Cast review | Diff source schema against Postgres 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 the loader summary. | New IDs continue and FKs hold. | Loader summary and insert log. | |
V-05Blocking | ORM and queries | Run the application test suite against Postgres. | No MySQL-only SQL remains on a critical path. | Test report. | |
V-06Blocking | Runtime connection | Connect from the production runtime, not only a laptop. | The app uses the intended Postgres URL. | Config audit. | |
V-07Blocking | Roles and IP allow | Connect as the app role and as a denied IP. | Least privilege. Denied IPs fail. | Access test. | |
V-08Blocking | Dump restore | Prove the pscale dump exists and PlanetScale 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, destination backups configured, and seven stable days.
- Archive the pscale dump and the loader log.
- Revoke the PlanetScale password used for the copy.
- Remove the copy host from allow lists you no longer need.
- Keep PlanetScale until billing and rollback close.
- Document the Postgres URLs.
- Schedule the next 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.
Varun Raj at FeatureOS left PlanetScale Vitess for self-hosted PostgreSQL in 2025. PlanetScale capped reads at 100,000 rows and 64 GB extracts, hid binary logs, and used passwords longer than 32 characters that cloud migrators rejected. They dumped with mydumper, loaded a bridge MySQL with myloader, then ran pgloader into Postgres. Writes paused for the snapshot, then they replayed later changes from created and modified timestamps. Total downtime was 10 minutes 31 seconds. They reported 80 to 90 percent faster queries and about half the database bill.
- If pgloader cannot talk to PlanetScale directly, dump into a short-lived MySQL bridge you control, then load Postgres from there.
- Pause writes, copy a snapshot, then replay rows changed after the dump using your own timestamps. You will not get binlogs.
- PlanetScale Vitess export limits, not SQL dialect, were the lock-in they hit first.
- A rehearsed snapshot plus timestamp replay kept downtime to minutes, not a long read-only window.
Adam Collier moved a PlanetScale MySQL database to a Coolify-hosted Postgres instance with pgloader. He pointed pgloader at the PlanetScale connection string with sslmode=require, created tables and indexes, reset sequences, and renamed the source schema to public. One resource.content column needed a cast to text. After pgloader finished, the data sat in the new Postgres database.
- Try pgloader against the PlanetScale connection string first, with SSL required, before you build a dump bridge.
- Cast awkward text columns in the load file and rename the source schema to public in the same run.
- A small PlanetScale database can move in one pgloader command if the host accepts the long password and SSL.
- Schema rename and sequence reset are part of the load, not a later cleanup surprise.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- PlanetScale: pscale database dump (Vitess)Accessed 2026-10-30
- PostgreSQL: SQL dumpAccessed 2026-10-30
- pgloader: MySQL sourceAccessed 2026-10-30
- PlanetScale: MySQL compatibilityAccessed 2026-10-30