PlanetScale → Supabase
Move a PlanetScale Vitess MySQL database into Supabase Postgres. Dump through pscale if live queries stop at 100,000 rows. Load with pgloader into the public schema. Turn on RLS. Do not use the transaction pooler on port 6543 for the load.
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
Supabase
- PostgreSQL, authentication, storage, realtime, and edge functions offer an open backend stack
- Managed infrastructure removes a large amount of operational work
- Production scaling and service integration still demand database and infrastructure judgment
- Platform-specific services can create architectural lock-in
Supabase: PostgreSQL, authentication, storage, realtime, and edge functions offer an open backend stack. 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: Production scaling and service integration still demand database and infrastructure judgment.
Jump to a section
Know the shape of the move.
This timeline assumes
- The source is PlanetScale Vitess (MySQL). A PlanetScale Postgres source is a different dump/restore path.
- You can pause writes. pgloader and dump/restore are snapshots, not live Vitess replicas.
- Supabase Auth, Storage, and Realtime are new products you enable on purpose. They do not appear from MySQL tables.
- The load uses a direct or session connection, not the transaction pooler on port 6543.
- We keep a pscale dump until Supabase 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 |
|---|---|---|---|---|
| Tables and rows | partial | critical | pgloader can copy schema and data. People dumping live PlanetScale have reported a 100,000-row query limit, which is why several write-ups dump with pscale first and load from local MySQL. | Use pscale database dump for the production snapshot, then pgloader from that dump or a local MySQL seed. |
| Schema name | partial | critical | pgloader often creates a schema named after the MySQL database. Supabase apps expect public. | ALTER SCHEMA ... RENAME TO public, or tell pgloader to rename, then confirm search_path. |
| Types | partial | critical | MySQL AUTO_INCREMENT, TINYINT, unsigned ints, enums, and zero dates do not have identical Postgres types. | Create the intended Postgres schema first if you use an ORM, then load data only, or review every pgloader cast. |
| Row Level Security | manual | critical | Imported tables are not a Supabase-secure default. One public migration write-up enables RLS after load so tables are not open through the anon key. | Enable RLS and write policies before you ship a client that uses the anon or service role keys. |
| Roles and GRANTs | manual | critical | Supabase uses postgres, authenticator, anon, authenticated, and service_role. pgloader objects may not be owned by the role your app uses. | Set schema ownership and grants after load. Test as anon and as authenticated, not only as postgres. |
| Auth, Storage, and Realtime | lost | high | PlanetScale has no Supabase Auth users, Storage buckets, or Realtime publications to import. | Stand up those products separately if the app needs them. Do not pretend MySQL users are auth.users. |
| PlanetScale branches and deploy requests | lost | high | Vitess branches do not become Supabase branches. | Use Supabase branching only if the plan includes it, and keep schema in versioned migrations. |
| Connection pooling | manual | critical | The transaction pooler on port 6543 is the wrong target for pgloader and for session features such as SET and prepared statements during load. | Use port 5432 or the session pooler for the import. Point the app at the pooler only after you test the driver. |
| ORM dialect | manual | critical | MySQL SQL and relationMode settings do not run unchanged on Postgres. | Change the provider, generate a Postgres schema, and run the app suite against a staging Supabase project. |
| Backups | manual | high | Supabase backup access depends on plan. A free project is not a substitute for the PlanetScale dump you just made. | Keep the pscale dump and confirm which Supabase backups you can restore before cutover. |
| SSL to PlanetScale | manual | high | Direct pgloader connections have failed on PlanetScale TLS for some operators. pscale connect to 127.0.0.1:3306 is a documented workaround. | Prefer a dump, or tunnel with pscale connect so pgloader talks to localhost. |
| Query insights and Vitess-specific SQL | lost | medium | Insights history and Vitess-only behavior stay behind. | Retest hot queries on Postgres EXPLAIN and keep the dump. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| PlanetScale Vitess database | Supabase Postgres database | manual | Create the project first. |
| MySQL database name | public schema | transform | Rename after pgloader if needed. |
| Table rows | public tables | transform | Load from a pscale dump when live selects hit the 100,000-row cap. |
| AUTO_INCREMENT | serial or identity | transform | Reset sequences. |
| TINYINT / unsigned / enum | approved Postgres types | transform | Do not accept silent text casts on money or status columns. |
| Table privileges | RLS plus grants to anon/authenticated | manual | Enable RLS before exposing keys. |
| pscale dump | local MySQL or pgloader source | transform | Keep the dump immutable. |
| Application DATABASE_URL | Supabase URI after verification | manual | Session versus transaction pooler is a separate choice. |
| PlanetScale password | No destination | unsupported | Rotate and stop using it after rollback closes. |
Make the move recoverable.
Create the source-of-truth backup
Hold a Vitess dump that does not depend on a live 100,000-row SELECT.
- Confirm the engine is Vitess and record table counts.
- Run pscale database dump for the production branch.
- Hash the dump directory and store it read-only.
- Create the Supabase project on a plan that can hold the data and the backups you need.
- Decide whether Auth or Storage will exist at all.
Proof to capture: A dated manifest lists dump hashes, counts, the Supabase plan, and the RLS decision.
Dump then pgloader
Avoid PlanetScale live row caps and TLS surprises.
- Dump the branch.
- Seed local MySQL if you cannot point pgloader at the dump directly.
- Load into Supabase with quote identifiers and a rename to public.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Supabase grants and RLS
Stop the Data API from exposing imported tables.
- Rename the schema to public if needed.
- Grant the roles the app actually uses.
- Enable RLS and add the first policies before you ship a browser key.
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.
Live SELECT stops at 100,000 rows
pgloader against live PlanetScale reports a row-count error or a short table.
- Consequence
- Supabase is missing most of the table.
- Mitigation
- Dump with pscale and load from that snapshot.
Stop if: Any scoped table is shorter than the PlanetScale count.
Tables land outside public
The SQL editor is pointed at public while data sits in another schema.
- Consequence
- The app looks empty.
- Mitigation
- Rename the schema and check search_path.
Stop if: The app cannot see a table that pgloader reported as loaded.
Anon key can read imported rows
RLS is off and a table is in the API.
- Consequence
- Customer data is public.
- Mitigation
- Enable RLS and test as anon before you ship the key.
Stop if: Anon can read a restricted table.
Do the work in this order.
- Days 1–3 · inventory
Inventory and plan
6–12 hours active2–3 days elapsedSchema review waiting- Confirm Vitess.
- List types, row counts, and whether Auth is in scope.
- Pick dump-then-load.
Depends on: PlanetScale and Supabase admin access
Stop / go checkpointDump?
Go when: Engine, size, and RLS plan are signed.
Stop when: The source is PlanetScale Postgres or Auth scope is unclear.
- Days 3–6 · backup
Dump
4–10 hours active2–3 days elapsedDump runtime waiting- Run pscale database dump.
- Hash files.
- Create the Supabase project.
Depends on: Approved inventory
Stop / go checkpointLoad?
Go when: Dump counts match PlanetScale.
Stop when: The dump is short.
- Days 6–14 · pilot
Load and harden
14–32 hours active5–9 days elapsedpgloader runtime waiting- Load into a staging project.
- Rename to public.
- Enable RLS and run the app suite.
Depends on: Verified dump
Stop / go checkpointCut over?
Go when: Counts, types, RLS, and app tests pass.
Stop when: Anon can read restricted data or a type is wrong.
- Days 14–18 · cutover
Freeze and switch
6–16 hours active1–3 days elapsedWrite freeze waiting- Pause PlanetScale writes.
- Take a final dump and load production.
- Switch the app URL.
Depends on: Passed pilot
Stop / go checkpointOpen writes?
Go when: Supabase is the only writer and counts match.
Stop when: A table is short or a writer is still on PlanetScale.
- Days 17–22 · observe
Observe and close
6–10 hours active4–6 days elapsedApplication cycle waiting- Watch errors, RLS, and pooler behavior.
- Keep the dump.
Depends on: Verified cutover
Stop / go checkpointClose rollback?
Go when: No trigger fires through one cycle.
Stop when: Data or RLS regresses.
Cut over with a way back.
Cutover
Make Supabase the only writable database after a frozen dump and load.
- Pause PlanetScale writes.
- Take a final pscale dump and hash it.
- Load production with the approved pgloader path.
- Rename to public, reset sequences, enable RLS, and reconcile counts.
- Switch DATABASE_URL and keep PlanetScale read-only.
Proof to capture: Supabase serves the app. Counts match. Anon cannot read restricted tables.
Rollback
Return writes to PlanetScale without losing reviewed Supabase-era rows.
- Stop writes to Supabase.
- Point the app back at PlanetScale.
- Export Supabase 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 Supabase is not receiving production writes.
- Short table
- Schema not public
- RLS failure
- Wrong pooler port in production
- App query failure
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | pscale snapshot | Compare dump table counts to PlanetScale. | No scoped table is short. | Dump manifest. | |
V-02Blocking | Loaded counts | Count rows in the intended Supabase schema. | Counts match the dump. | Count sheet. | |
V-03Blocking | public search_path | List tables as the app role. | The app sees every scoped table in public. | \dt output. | |
V-04Blocking | Cast review | Inspect TINYINT, enum, unsigned, and datetime columns. | Approved types only. | Type ledger. | |
V-05Blocking | RLS | Query as anon and as authenticated. | Restricted tables deny anon. | Policy test. | |
V-06Blocking | Pooler port | Inspect the import URL and the app URL. | Import did not use transaction pooler 6543. App URL is the approved one. | Config audit. | |
V-07Blocking | ORM suite | Run application tests against Supabase. | No MySQL-only SQL on a critical path. | Test report. | |
V-08Blocking | Dump and rollback URL | Confirm the dump exists and PlanetScale still accepts the old URL. | You can point the app back. | Rollback note. |
Remove the scaffolding safely.
Safe after: One application cycle, RLS tests passing, and seven stable days.
- Archive the pscale dump and pgloader log.
- Revoke PlanetScale passwords used for the move.
- Remove temporary SQL functions and migration users.
- Keep PlanetScale until billing and rollback close.
- Document direct versus pooler URLs.
- Schedule the next Supabase backup and policy 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.
Hein Zaw left PlanetScale Hobby after the $47 Scaler Pro jump. He tunneled with pscale connect, loaded MySQL into local Postgres with pgloader, then rewrote Drizzle from mysqlTable to pgTable. Inserts needed returning() because Postgres does not give MySQL insertId. He dumped data-only SQL with sequence setval lines, tested two files locally, then applied schema and data on Supabase. He flags the 500 MB Supabase free storage cap versus PlanetScale's old 5 GB, and the lack of free restore access.
- Load into local Postgres and pass the application test suite before you touch the Supabase project.
- Keep sequence setval lines from pg_dump. Postgres will not continue MySQL autoincrement for you.
- The ORM rewrite, not pgloader, was the long pole. Last-insert-id and datetime types all changed.
- Supabase free tier is not a feature match for PlanetScale Hobby storage, backups, or branching.
Abhishek Saha left PlanetScale Hobby for cost. A Colab notebook could not open a TLS PlanetScale session. pgloader worked after pscale connect exposed MySQL on 127.0.0.1:3306. He pushed the Prisma schema first, then loaded data only into public and set the schema owner to postgres to avoid permission errors. Because Supabase free tier withholds backup restore, he added a cron pg_dump of the public schema.
- Create tables with your ORM first, then tell pgloader to copy data only into public owned by postgres.
- If you need restores on Supabase free tier, schedule your own pg_dump. The product keeps backups you cannot download.
- PlanetScale TLS blocked the notebook path. A local pscale tunnel unblocked pgloader.
- Losing first-party restore access is a product gap you have to replace, not a footnote.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Supabase: migrating to SupabaseAccessed 2026-10-29
- PlanetScale: pscale database dump (Vitess)Accessed 2026-10-29
- Supabase: connect with PostgresAccessed 2026-10-29