# Tutorial: move a Supabase app to Sovaria
Source: https://sovaria.in/docs/tutorial-migrate-from-supabase

A step-by-step move of a real Supabase app: dry run, import, check the data and the rules, keep your users' passwords, switch the app's client code, and plan the cut-over.

This walks through a whole move, from the first dry run to switching traffic. What the importer does and how each piece maps is in [Migrate from Supabase or Firebase](https://sovaria.in/docs/migrate-from-supabase-or-firebase#supabase); this page is the order to do it in, and how to check each step.

## 1. Prepare

- Create the Sovaria project in the region nearest your Supabase project, so the copy is fast and latency stays the same.
- Install the CLI and sign in as an owner or admin: `npm install -g @sovaria/cli`, `sov login`, `sov scope pick`.
- Install `pg_dump` and `psql` 15 or newer. Copy the Supabase **database connection string** (Project settings › Database) and the **service_role key** (for storage).
- Note what the importer doesn't move: Edge Functions (Deno code), cron jobs, database webhooks and auth email templates. You will recreate those in step 6.

## 2. Dry run

Terminal:
```bash
export SUPABASE_DB='postgresql://postgres:PASSWORD@db.REF.supabase.co:5432/postgres'
sov import supabase --db-url "$SUPABASE_DB" \
  --supabase-url https://REF.supabase.co --service-key "$SUPABASE_SERVICE_ROLE_KEY" --dry-run
```

Nothing is written. Read the report: extensions to turn on first ([Extensions](https://sovaria.in/docs/database-extensions)), policies that will be rewritten (`TO authenticated` → `auth.uid() IS NOT NULL`), and anything it can't import.

## 3. Import

Terminal:
```bash
sov import supabase --db-url "$SUPABASE_DB" \
  --supabase-url https://REF.supabase.co --service-key "$SUPABASE_SERVICE_ROLE_KEY"
# parts on their own: --only schema,auth   (then --only data,storage)
```

It runs schema, then users, then data, then storage, and writes `sovaria-import-report.json`. The schema arrives as one migration, so it shows in [Version history: see, undo and restore changes](https://sovaria.in/docs/version-history) and can be rolled back as a unit.

## 4. Check that everything arrived

1. **Row counts** — Run the same count on both sides and compare.
```sql
select relname as table, n_live_tup as rows
from pg_stat_user_tables where schemaname = 'public' order by relname;
```
2. **Passwords** — Sign in as a test user with their **old** password through the SDK (`await sovaria.auth.signIn(email, oldPassword)`). Supabase's bcrypt hashes are used as they are, so nobody has to reset.
3. **Rules** — Run a simulation: every table as a visitor, a signed-in user and each role, compared with what your policies intend.
```bash
sov sim run --fail-on critical
```
4. **Storage** — Compare object counts per bucket in Storage. Bucket privacy (public or private) is copied.

## 5. Switch the app

Table and function calls through supabase-js keep working against Sovaria's REST endpoint ([HTTP API](https://sovaria.in/docs/http-api)). Auth moves to the Sovaria SDK, and supabase-js sends its session token through its `accessToken` option (supabase-js 2.45 or newer).

Before:
```ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
await supabase.auth.signInWithPassword({ email, password })
const { data } = await supabase.from('notes').select('id, body').order('id', { ascending: false })
```

After:
```ts
import { createClient as createSovaria } from '@sovaria/sdk'
import { createClient } from '@supabase/supabase-js'

export const sovaria = createSovaria('https://<project-ref>.sovarias.in', '<anon-key>')

// supabase-js for tables and rpc, with Sovaria's session.
export const supabase = createClient('https://<project-ref>.sovarias.in', '<anon-key>', {
  accessToken: async () => sovaria.auth.getSession()?.access_token ?? null,
})

await sovaria.auth.signIn(email, password)
const { data } = await supabase.from('notes').select('id, body').order('id', { ascending: false })
```

- Moving queries to `sovaria.data.sql` or `sovaria.data.rpc` over time is optional. Both run under the same rules.
- Storage calls move to `sovaria.storage` ([Working with files](https://sovaria.in/docs/storage-files)). File paths are kept, so stored paths in your tables still point at the right files.
- Add your site's origin under Project settings › API ([Allowed origins (CORS) for browser apps](https://sovaria.in/docs/allowed-origins)) and register mobile apps and redirect URLs under Authentication.

## 6. Recreate functions, schedules and webhooks

| Supabase | Sovaria |
|---|---|
| Edge Function (Deno) | A function in Node.js, TypeScript on Bun, Python or Go. `Deno.env.get` becomes `process.env`, and `Deno.serve` becomes `export async function handler(event, context)` ([Functions](https://sovaria.in/docs/functions-overview)). |
| pg_cron job | A function schedule ([Event triggers](https://sovaria.in/docs/function-triggers)). |
| Database webhook | A database webhook or a `db.row` trigger ([Database webhooks](https://sovaria.in/docs/database-webhooks)). |
| Auth hooks | Auth hooks, including blocking before-sign-up and before-sign-in hooks ([Auth event hooks](https://sovaria.in/docs/auth-hooks), [Blocking hooks (before create, before sign-in)](https://sovaria.in/docs/auth-blocking-hooks)). |
| Email templates | Authentication › Emails ([Auth emails](https://sovaria.in/docs/auth-emails)). |

## 7. Cut over

- The importer copies a snapshot; it doesn't keep the two projects in sync. Plan a short write freeze: put the app in read-only mode, run the data import into a fresh project (or `--only data,storage` after clearing the rehearsal's tables), check counts, then release the new app version.
- Rehearse the whole sequence once on a copy so you know how long the freeze takes.
- Keep the Supabase project read-only for a week or two as a fallback before you delete it.
