# Move a self-hosted Supabase to SupaHost

> Move a self-hosted Supabase (Docker Compose) project to SupaHost: users with their original IDs and passwords, your schemas and data, Storage files, Edge Functions and cron jobs.

Source: https://supahost.app/kb/migrate-from-self-hosted · Updated 2026-11-11

The one-click import only reads from Supabase Cloud, so a self-hosted Supabase moves in parts: Auth users through the admin API, your own schemas with `pg_dump`, Storage files through the Storage API, and functions and cron jobs by hand. Because both sides run the same open-source stack, each part is direct and your app code barely changes.

## Plan

- **Order matters.** Load users first, keeping their UUIDs, so foreign keys to `auth.users` in your tables still match. Then your schemas and data, then files.
- **Freeze writes** on the source for the final pass (see [Freezing writes during a migration](/kb/read-only-mode)).
- **Size.** Your data must fit the plan's shared quotas (database 8 / 25 / 50 GB and files 100 / 250 / 500 GB on Builder / Pro / Business).
- **Large databases** are easiest on Business, where `pg_restore` runs over the SSH tunnel.

## 1. Users, with IDs and passwords

Self-hosted Supabase stores bcrypt hashes in `auth.users.encrypted_password`, and the admin API accepts both a hash and a fixed `id`:

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

const src = new pg.Client({ connectionString: process.env.SOURCE_DB_URL })
const dst = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
  auth: { persistSession: false, autoRefreshToken: false },
})
await src.connect()
const { rows } = await src.query(
  `select id, email, encrypted_password, email_confirmed_at, raw_user_meta_data, raw_app_meta_data
   from auth.users where email is not null`
)
for (const u of rows) {
  const { error } = await dst.auth.admin.createUser({
    id: u.id,
    email: u.email,
    email_confirm: !!u.email_confirmed_at,
    ...(u.encrypted_password ? { password_hash: u.encrypted_password } : {}),
    user_metadata: u.raw_user_meta_data,
    app_metadata: { roles: u.raw_app_meta_data?.roles },
  })
  if (error) console.error(u.email, error.message)
}
```

OAuth identities aren't copied this way; users who signed in with Google or GitHub are linked again when they sign in with a matching verified email. See [Moving Auth users between projects](/kb/migrate-auth-users-between-projects) for more detail.

## 2. Schemas and data

Dump only the schemas you own, never `auth`, `storage` or the other Supabase schemas, which already exist in the SupaHost project:

```bash
pg_dump -Fc --no-owner --no-privileges -n public -n billing \
  "postgresql://postgres:<pw>@selfhost.internal:5432/postgres" -f app.dump
```

**Business and Enterprise:**

```bash
ssh -N -L 6543:127.0.0.1:5432 supahost@acme.supahost.space &
pg_restore --no-owner --no-privileges -d "postgresql://postgres:<password>@127.0.0.1:6543/p_app_one" app.dump
```

**Builder and Pro:** run the schema (`pg_restore --schema-only -f schema.sql app.dump`) in Studio's SQL editor or with MCP `apply_migration`, then load table data from CSV in the table editor, or insert rows with supabase-js and the `service_role` key. Only `public`, `storage`, `graphql_public` and `pgmq_public` are reachable through the Data API, so data for other schemas has to go in through SQL.

The dump includes your RLS policies. Recreate any policies or triggers you added on `storage.objects` separately.

## 3. Storage files

Create the same buckets, then copy each object: list it on the source with `storage.from(bucket).list(folder)` (recursing into folders), `download()` it, and `upload()` it to SupaHost with the same path. Files can be up to 50 MB each on SupaHost.

## 4. Functions, cron and settings

- **Edge Functions:** paste each `volumes/functions/<name>/index.ts` into Dashboard → your project → **Functions** → New function, or deploy over MCP. Add secrets in the Secrets card.
- **Cron jobs:** list them on the source with `select jobname, schedule, command from cron.job;` and recreate them with `cron.schedule(...)`. Don't run `create extension pg_cron`; it's built in.
- **Auth settings:** set the Site URL, redirect URLs, providers and email templates in the **Auth** tab. SMTP is handled by SupaHost.

## Switch and verify

Point the app at `https://app-one.acme.supahost.space` with the new anon key. The JWT secret is different, so users sign in once more. Compare user, row and object counts with the source, and test sign-in, uploads and each function.

## Related

- [Importing data (CSV, SQL dumps)](/docs/database/import-data)
- [Deploying functions](/docs/functions/deploying)
- [Scheduled jobs with pg_cron](/docs/cron-queues/cron)
- [Supabase features on SupaHost](/docs/getting-started/feature-support)
