Skip to main content
Percher is still being built and account creation is paused — get notified when it opens.

Migrating from Supabase

Keep Supabase or switch to PocketBase

Ask your agent
Plan how to move my Supabase app to Percher. Inventory its database, auth, storage, functions, and secrets first; keep the existing app working until I approve the cutover.Read the guide at percher.app/docs/migrate-supabase
Keep my Supabase, but host the app on Percher.Read the guide at percher.app/docs/migrate-supabase
How it works, with exact details

Two options: keep using your Supabase project as an external database, or switch to Percher's managed PocketBase. For most apps — and anything that leans on Postgres features — Option 1 is the right call.

Option 1: Keep Supabase

# percher.toml
[data]
mode = "supabase"

[data.supabase]
url = "https://your-project.supabase.co"
anon_key = "eyJ..."

# Use only a public client key, never a service-role or secret key.

Your data and Supabase services stay at Supabase. Match the environment-variable names your app reads, check the HTTPS connection method, and update authentication redirects. Follow the connection guide before publishing.

Option 2: Switch to PocketBase

Percher generates most of the migration for you. It writes a ./migration-preview/ folder — nothing touches your data until you run the scripts yourself.

Honest limits — read before committing

  • Auth users can't be moved directly. Supabase password hashes aren't compatible with PocketBase — every user has to reset their password.
  • It's a preview plus a script, not one click. You run the data-import yourself with a Supabase service-role key and review the flagged items.
  • Some things don't port at all: Edge Functions, Postgres views/triggers/functions, complex RLS (the tool translates the simple auth.uid() = column case and flags the rest), realtime, PostGIS, and pgvector. If your app depends on these, keep Supabase (Option 1).
Generate the migration preview
bunx percher migrate-from-supabase --project <ref> --token sbp_...
#  → migration-preview/pb_schema.json    tables → PocketBase collections
#  → migration-preview/pb_migrate.js     runnable data-import script (copies your rows)
#  → migration-preview/MIGRATION_NOTES.md every RLS policy + manual step

Review MIGRATION_NOTES.md first — it lists every RLS policy, flagged column, and manual step. Then run the data-import script yourself with a Supabase service-role key.

Rewrite your Supabase SDK calls automatically
# Also convert your Supabase SDK calls to the PocketBase SDK:
bunx percher migrate-from-supabase --project <ref> --token sbp_... --rewrite-client
#  preview first; add --apply to write in place (originals are backed up)
#  scaffolds src/lib/pocketbase.ts + inserts imports; auth recipes in REWRITE_NOTES.md

# See what's still on Supabase vs PocketBase (local scan, no token):
bunx percher migrate-from-supabase --status

The rewrite converts the high-confidence auth + CRUD calls. Anything with no clean equivalent (storage, rpc, realtime, edge functions) is left untouched and flagged in REWRITE_NOTES.md, never stubbed.

Hybrid is fine and expected. While any Supabase call remains, the migration keeps both SDKs and both env sets — that's correct, not a missed step. --status is the local scoreboard: it reports remaining @supabase imports, undefined pb references, and the project's overall state (broken / hybrid / not-started / complete). Drop @supabase/supabase-js only once nothing Supabase is left.

Concept mapping for the parts you rewrite by hand
supabase.from('table').select()pb.collection('table').getList()
supabase.from('table').insert({})pb.collection('table').create({})
supabase.from('table').update({})pb.collection('table').update(id, {})
supabase.from('table').delete()pb.collection('table').delete(id)
supabase.auth.signUp()pb.collection('users').create({})
supabase.auth.signInWithPassword()pb.collection('users').authWithPassword()
supabase.auth.getUser()pb.authStore.record
supabase.storage.upload()pb.collection('x').create(formData)
supabase.channel().subscribe()pb.collection('x').subscribe('*', fn)
Row Level Security (SQL policies)PocketBase API rules (per collection)
.select('*, posts(*)')pb.getList({ expand: 'posts' })