← All playbooks
Databases migration

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.

Typical timeline8–20 business days30–70 hours active work
Statusneeds review
Source profilePlanetScale documentation reviewed 2026-10-30
Destination profilePostgreSQL documentation reviewed 2026-10-30
Documentation date
Review window
Evidence4 sources
This route needs review.Use it as a planning baseline, then verify the dated sources and test with representative data before production.
Before you migrate

Should you make this move?

Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.

Current platform

PlanetScale

Reasons to stay
  • 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
Reasons to leave
  • 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
New platform

PostgreSQL

What gets better
  • Powerful SQL, extensions, integrity, and open governance support demanding workloads
  • A mature data model and query ecosystem supports serious production workloads
What gets worse
  • Tuning, upgrades, and sophisticated features require experienced operators
  • Schema design, migration, tuning, and operations require specialist attention
Best of the move

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.

Worst of the move

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
01At a glance

Know the shape of the move.

Transfer outcome12 features audited
Transfer outcome distributionClean transfer: 0, Partial transfer: 5, Manual rebuild: 3, Not transferred: 4.
Clean0
Partial5
Manual3
Lost4
Mapping route8 of 9 fields have a destination path

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.
02What transfers

What survives the move.

“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.

ItemOutcomeImpactWhat happensMitigation
Schemas and tablespartialcriticalpgloader 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 datapartialcriticalA 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.
Typespartialcriticalpgloader 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 sequencespartialcriticalpgloader 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 routineslosthighPlanetScale 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 requestslosthighVitess branches do not become Postgres branches.Create destination branches yourself if you still want branch-per-PR.
Vitess connection and sharding behaviorlosthighVitess limits and PlanetScale pooling stay behind.Retest queries that depended on Vitess or the PlanetScale pooler.
Users and IP allow listsmanualcriticalMySQL users do not become Postgres roles.Create least-privilege roles and allow only the app and the copy host.
ORM SQL dialectmanualcriticalPrisma, 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 windowslostmediumPlanetScale insights and Vitess backup history stay in PlanetScale.Take a pscale dump first. Configure destination backups before traffic.
Foreign keys skipped by some loaderspartialhighSome 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 proxiesmanualhighApps 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.
03Field and feature mapping

Where each thing goes.

SourceDestinationMethodNotes
PlanetScale databasePostgreSQL databasemanualCreate the destination name before the load.
MySQL schema namePostgres schema, often renamed to publictransformDo not leave search_path pointing at the old name.
AUTO_INCREMENTserial or identitytransformReset sequences after load.
TINYINTboolean unless you CAST otherwisetransformCheck columns you queried as integers.
DATETIMEtimestamptransformConfirm time zone handling.
Primary and foreign keysPostgres constraintstransformRead the loader summary.
Direct PlanetScale MySQL URLpgloader source with TLStransformUse the direct connection, not a pooled one.
Application DATABASE_URLPostgres URL after verificationmanualSwitch only after counts and app tests pass.
Vitess branch or deploy requestNo destinationunsupportedRecreate destination branches if you still want them.
04Before you begin

Make the move recoverable.

Backup procedure

Create the source-of-truth backup

Keep a Vitess dump you can restore even if pgloader or the new host fails.

  1. Confirm the engine is Vitess, not PlanetScale Postgres.
  2. Record table counts and sizes.
  3. Run pscale database dump against the production branch.
  4. Export the schema separately for type review.
  5. 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.

Transformation · A .load file plus pgloader

pgloader config

Copy MySQL tables into PostgreSQL with explicit casts.

  1. Use the PlanetScale direct MySQL string with sslmode=require.
  2. Point at a Postgres URL the loader can reach.
  3. Rename the schema to public if the app expects that.
  4. Add CAST rules you already approved.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

Transformation · ORM provider plus the application test suite

App dialect change

Stop shipping MySQL SQL to Postgres.

  1. Point a branch at the new database.
  2. Change the provider and regenerate migrations.
  3. Run queries that used MySQL-only functions.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

05Handle with care

The things most likely to hurt.

These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.

Cutover

Writes continue during the copy

criticalpossible likelihood

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.

Pilot

TINYINT becomes boolean

highpossible likelihood

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.

Inventory

You run pgloader against PlanetScale Postgres

criticalpossible likelihood

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.

App

Edge runtime cannot open a Postgres socket

highpossible likelihood

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.

06Precise timeline

Do the work in this order.

Estimate forOne Vitess database that fits a planned write freeze
Total elapsed8–20 business days
Active work30–70 hours
BufferAdd time for large tables, a wide ORM rewrite, or an edge-runtime proxy.
01
Days 1–3Inventory and types2–3 days
02
Days 3–5Dump and allow lists1–3 days
03
Days 5–12pgloader rehearsal4–8 days
04
Days 12–16Freeze and copy1–3 days
05
Days 15–20Observe and close4–6 days
  1. 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 checkpoint

    Dump?

    Go when: Engine, size, and type map are signed.

    Stop when: The source is PlanetScale Postgres.

  2. 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 checkpoint

    Pilot?

    Go when: The dump hashes and the destination exist.

    Stop when: The dump is incomplete.

  3. 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 checkpoint

    Cut over?

    Go when: Counts, types, and app tests pass.

    Stop when: A critical type or query is wrong.

  4. 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 checkpoint

    Open writes?

    Go when: Postgres is the only writer and counts match.

    Stop when: A row is missing or a writer is still on PlanetScale.

  5. 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 checkpoint

    Close rollback?

    Go when: No trigger fires through one cycle.

    Stop when: Data or auth queries regress.

07The point of change

Cut over with a way back.

Go live

Cutover

Make PostgreSQL the only writable database after a frozen copy.

Recommended window: A low-write window long enough for the copy plus count checks.

  1. Pause application and job writes to PlanetScale.
  2. Take a final pscale dump.
  3. Run the approved pgloader config.
  4. Reconcile counts, sequences, and a recent-row sample.
  5. Switch DATABASE_URL and keep PlanetScale read-only.

Proof to capture: Postgres serves the app. Counts match. No writer is still on PlanetScale.

Return to safety

Rollback

Return writes to PlanetScale without losing Postgres-era rows you still want.

Deadline: Before you drop the PlanetScale branch or the pscale dump.

  1. Stop writes to Postgres.
  2. Point the app back at PlanetScale.
  3. Export Postgres rows written after cutover.
  4. Apply only reviewed rows to PlanetScale.
  5. Run the same app checks on PlanetScale.

Proof to capture: PlanetScale is writable again and Postgres is not receiving production writes.

Rollback immediately when
  • Count mismatch
  • Type or sequence error
  • App query failure
  • Write landed on the wrong host
  • The loader did not finish
08Verification report

Prove the migration worked.

Every blocking check must pass. Capture the evidence before cleanup begins.

0%
Interactive report preview0 / 8 checks passed
PassIDCheckMethodExpected resultEvidence
V-01BlockingVitess sourceConfirm the PlanetScale engine is Vitess/MySQL.Source is MySQL-compatible Vitess.Console screenshot.
V-02BlockingRow countsCompare table counts after the freeze.Every scoped table matches or has a signed exception.Count sheet.
V-03BlockingCast reviewDiff source schema against Postgres for TINYINT, DATETIME, and AUTO_INCREMENT columns.Every critical column has the approved type.Type ledger.
V-04BlockingSequences and foreign keysInsert a row and inspect the loader summary.New IDs continue and FKs hold.Loader summary and insert log.
V-05BlockingORM and queriesRun the application test suite against Postgres.No MySQL-only SQL remains on a critical path.Test report.
V-06BlockingRuntime connectionConnect from the production runtime, not only a laptop.The app uses the intended Postgres URL.Config audit.
V-07BlockingRoles and IP allowConnect as the app role and as a denied IP.Least privilege. Denied IPs fail.Access test.
V-08BlockingDump restoreProve the pscale dump exists and PlanetScale still accepts a rollback URL.You can point the app back.Dump path and rollback note.
09Post-migration cleanup

Remove the scaffolding safely.

Safe after: One application cycle, destination backups configured, and seven stable days.

  1. Archive the pscale dump and the loader log.
  2. Revoke the PlanetScale password used for the copy.
  3. Remove the copy host from allow lists you no longer need.
  4. Keep PlanetScale until billing and rollback close.
  5. Document the Postgres URLs.
  6. Schedule the next backup and role review.
10Experiences from the field

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.

First-person accountVarun Raj · FeatureOS

Why We Left PlanetScale and Self-Hosted Our Database2025

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.

What they recommend
  • 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.
Worth noticing
  • 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.
First-person accountAdam Collier · Adam Collier

Transitioning from Planetscale to Self-Hosted Postgres with Coolify and HTTP Proxy2024

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.

What they recommend
  • 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.
Worth noticing
  • 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.
Sources and evidence

Verify against the primary material.

Platform behavior changes. Check these sources and the review dates above before executing a production migration.

  1. PlanetScale: pscale database dump (Vitess)Accessed 2026-10-30
  2. PostgreSQL: SQL dumpAccessed 2026-10-30
  3. pgloader: MySQL sourceAccessed 2026-10-30
  4. PlanetScale: MySQL compatibilityAccessed 2026-10-30