Migrating from Supabase
Keep Supabase or switch to PocketBase
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() = columncase 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.recordsupabase.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' })