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.
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.usersin 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).
- 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_restoreruns 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:
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 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:
pg_dump -Fc --no-owner --no-privileges -n public -n billing \
"postgresql://postgres:<pw>@selfhost.internal:5432/postgres" -f app.dump
Business and Enterprise:
ssh -N -L 6543:127.0.0.1:5432 [email protected] &
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.tsinto 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 withcron.schedule(...). Don't runcreate 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.