Migrate from Supabase
Move Supabase database, auth, storage and realtime calls onto MainSDK db, auth, files and events, then keep what has no interface behind raw().
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Move Supabase database, auth, storage and realtime calls onto MainSDK db, auth, files and events, then keep what has no interface behind raw(). Your Supabase account stays; nothing here deletes it.
Before you start:
- A git repository with a clean working tree.
mainsdk migrateworks on a branch and never touches yours. - Node 22 and
npx mainsdk. Nomain.yamlyet?migratewrites one; or start with Adopt an existing app. - Your Supabase keys stay where they are. Move them to
secret:references when the driver is chosen (see Secrets).
1. Read the report
npx mainsdk migrate supabase --report-onlyThe report writes no file. It sorts every Supabase call in your code into three lists: mapped (an interface call replaces it), raw (it stays behind raw(), with the reason) and needs a person (a policy, a rule file or a call with no interface). Add --json for a machine-readable report.
For the sample app of this guide (packages/cli/test/fixtures/migrate/supabase), the report finds 6 mapped, 1 raw and 1 for a person.
2. What the codemod changes
| Where | Before | After |
|---|---|---|
lib/auth.ts:4 | return supabase.auth.signInWithPassword({ email, password }) | auth.signIn() |
lib/auth.ts:8 | return supabase.auth.getUser() | auth.require(req) |
lib/db.ts:4 | return supabase.from('orders').select('*') | db.orders.for(user).list() |
lib/db.ts:8 | return supabase.from('orders').insert(row) | db.orders.for(user).create() |
lib/db.ts:12 | return supabase.from('orders').delete().eq('id', id) | db.orders.for(user).remove() |
lib/files.ts:4 | return supabase.storage.from('avatars').upload(path, file) | files.upload() |
Each rewrite is mechanical and one line at a time. Imports of Supabase go when no call needs them, and mainsdk is added to package.json.
3. What stays behind raw()
lib/realtime.ts:3: no broadcast driver capability: presence
A call behind raw() keeps working through the Supabase host on your network allow list, and mainsdk check still sees it. Each one has a marker comment, // mainsdk migrate: ..., so you can find it later.
4. What needs a person
supabase/policies.sql:1: row-level security policy needs a data label owner
The codemod leaves these lines alone and marks them. No tool should guess a policy.
5. Stateful moves are guided jobs
Data does not move with the code. Each stateful move is a job with steps and a rollback, listed in the pull request and never run by the codemod.
users: Move auth.users to the new auth driver
- Export users from the Supabase dashboard or admin API into users.jsonl (ids and emails; password hashes only where Supabase exports them).
- Run the import against a staging project and compare the counts.
- Switch auth.driver for 1% of sign-ins, then all.
Rollback: Set auth.driver back to supabase in main.yaml; Supabase still holds every user until you delete the project.
rows: Copy each table to the app database
- Take pg_dump of each table named in the report.
- Restore it into the new database and run the row-count check.
- Dual-write for one week, then read from the new database.
Rollback: Point db.driver back at the Supabase connection string; the dual-write kept it current.
6. Open the pull request
npx mainsdk migrate supabaseThis records your sign-in and checkout flows as locked flows first, then creates the branch migrate/supabase, applies the codemods, adds .github/workflows/mainsdk-migrate.yml, runs mainsdk check and the locked flows, commits, pushes and opens one pull request with gh. If a gate fails, the branch is kept and no pull request opens (MAIN-MIGRATE-0001). --no-pr stops after the commit.
Locked flows run again in CI: npx mainsdk migrate flows.
Where you end
You end at the reference architecture mainsdk/saas-web: the same shape, with its tests, benchmark and chaos drills.
npx mainsdk init --template mainsdk/saas-webWas this page helpful?