---
title: "Migrate from Supabase"
description: "Move Supabase database, auth, storage and realtime calls onto MainSDK db, auth, files and events, then keep what has no interface behind raw()."
section: "guides"
status: "Planned for 0.7"
spec: "migration-guides"
canonical: "https://mainsdk.dev/docs/guides/migrate-from-supabase"
updated: "2026-10-28"
---

# 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: Planned for 0.7 · Canonical: https://mainsdk.dev/docs/guides/migrate-from-supabase · 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](https://mainsdk.dev/docs/getting-started/existing-app).
- Your Supabase keys stay where they are. Move them to `secret:` references when the driver is chosen (see [Secrets](https://mainsdk.dev/docs/interfaces/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

| 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

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`](https://mainsdk.dev/docs/guides/reference-saas-web): the same shape, with its tests, benchmark and chaos drills.

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