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.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).
  • 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:

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

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us