MainSDK
Search⌘K
How-to

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().

Planned for 0.7

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.
Stability levels
Updated 2026-10-28

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 migrate works on a branch and never touches yours.
  • Node 22 and npx mainsdk. No main.yaml yet? migrate writes 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

text
npx mainsdk migrate supabase --report-only

The 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

WhereBeforeAfter
lib/auth.ts:4return supabase.auth.signInWithPassword({ email, password })auth.signIn()
lib/auth.ts:8return supabase.auth.getUser()auth.require(req)
lib/db.ts:4return supabase.from('orders').select('*')db.orders.for(user).list()
lib/db.ts:8return supabase.from('orders').insert(row)db.orders.for(user).create()
lib/db.ts:12return supabase.from('orders').delete().eq('id', id)db.orders.for(user).remove()
lib/files.ts:4return 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

  1. Export users from the Supabase dashboard or admin API into users.jsonl (ids and emails; password hashes only where Supabase exports them).
  2. Run the import against a staging project and compare the counts.
  3. 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

  1. Take pg_dump of each table named in the report.
  2. Restore it into the new database and run the row-count check.
  3. 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

text
npx mainsdk migrate supabase

This 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.

text
npx mainsdk init --template mainsdk/saas-web

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet