scribase import
Three shapes share one verb:
scribase import <url>— reads an existing Postgres database and produces a Scribase schema file.scribase import supabase ...— migrates a live Supabase project (the backend your app reaches with supabase-js) into a Scribase environment via the management API.scribase import postgres ...— migrates a plain Postgres database (a Neon branch, Render, RDS, or self-hosted Postgres — anything with no Supabase-style Auth/Storage API alongside it) into a Scribase environment.
Import a schema from a URL
Connects to an existing Postgres database, introspects its columns, constraints,
and RLS policies, and emits a .scribase schema file.
scribase import <connection-url> [--out FILE] [--partial]Options
| Flag | Description |
|---|---|
--out <file> |
Write output to file instead of stdout |
--partial |
Allow incomplete import (some columns may not be expressible) |
Example
scribase import "postgresql://user:pass@host/db" --out schema.scribaseNotes
- Any column type, constraint, or default that cannot be expressed in the Scribase schema language is printed as a fidelity note. Without
--partial, the command fails if there are any such notes. - Tables without row-level security are printed as security notes — they are imported but the output warns you.
- Use
--partialwhen working with complex legacy schemas and iterating toward full coverage.
Migrate a live project
Migrates a live project (the backend your app reaches with supabase-js) into a Scribase environment. The source project's schema and data are read and applied to the destination environment.
scribase import supabase \
--project-ref <supabase-project-ref> \
--org <scribase-org-id> \
--project <scribase-project-id> \
--env <scribase-env-id> \
[--connection-string <url>] \
[--service-key <key>] \
[--dry-run]Options
| Flag | Env override | Description |
|---|---|---|
--project-ref |
— | Source project reference ID |
--org |
— | Destination Scribase organization ID |
--project |
— | Destination Scribase project ID |
--env |
— | Destination Scribase environment ID |
--connection-string |
SCRIBASE_SOURCE_CONNECTION_STRING |
Source database URL |
--service-key |
SCRIBASE_SOURCE_SERVICE_KEY |
Source service_role key |
--schemas a,b |
— | Copy only these schemas (default: every application schema) |
--no-storage |
— | Skip buckets and objects |
--dry-run |
— | Plan what would move without touching anything |
--no-wait |
— | Start the import and print its import_id without following progress |
--functions-dir DIR |
— | After the data import succeeds, deploy every edge function in this local supabase/functions directory (ignored with --dry-run) |
--functions-only |
— | Skip the data import and only deploy the functions (--functions-dir defaults to supabase/functions) |
The connection string and service key are secrets. Pass them via environment variables rather than on the command line in production contexts.
Example
export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://..."
export SCRIBASE_SOURCE_SERVICE_KEY="eyJhbGci..."
scribase import supabase \
--project-ref abcdefghijkl \
--org acme \
--project store \
--env production \
--dry-runThe server returns a report describing what would be imported. Remove --dry-run to execute: the server starts the import in the background, and the command follows GET /v1/imports/{import_id}, printing each phase as it starts, then prints the verified report. Pressing Ctrl-C stops watching, not the import.
Edge functions
# Data first, then every function in the local checkout:
scribase import supabase --project-ref <ref> --org acme --project store --env production \
--functions-dir supabase/functions
# Or only the functions, for example after a data import that already finished:
scribase import supabase --functions-only --org acme --project store --env production \
--functions-dir supabase/functionsFunction secrets are never read from the source. Re-enter them with
scribase secrets set.
What is migrated
In order, each step replay-safe (--resume <import_id> continues from the last committed step):
- Extensions the app schemas use (best effort; any the target cannot install is listed as an action item).
- Schema of every application schema (
publicplus any schema that the source platform or an extension does not own): tables, types, functions and RPCs, views, indexes,ENABLE ROW LEVEL SECURITY. Foreign keys and triggers are held back until after the data, aspg_restoredoes. - Grants: the exact privileges
anon,authenticatedandservice_rolehold on every schema, table, sequence and function (revokes included, so an RPC hidden fromanonstays hidden). - Auth:
auth.userswith ids and bcryptencrypted_passwordhashes byte for byte, plusinstance_id,audandrole(the lookup columns GoTrue uses on sign-in);auth.identities(provider,provider_id,identity_data), so Apple and GooglesignInWithIdTokenusers resolve to the same user id;auth.mfa_factors(TOTP secrets verbatim). - Data, page by page, keyset-paged and idempotent.
- Post-data: foreign keys, table triggers, sequence positions (
setval), and triggers onauth.users/storage.objectsthat call app functions (for exampleon_auth_user_created). - RLS policies on the app schemas and on
storage.objects/storage.buckets, with theirauth.uid()/auth.jwt()expressions unchanged. - Storage: buckets, then each object's bytes through the destination environment's storage API (
x-upsert, so a resume overwrites), then the object's originalowner_id, so owner-based storage policies keep matching. - Verification: per-table row counts and checksums, policy parity, auth user and identity counts, object counts, and an optional login proof.
What is not migrated
- Sessions and refresh tokens. Scribase signs with its own JWT secret, so every existing session ends. Users sign in once more after the switch, with the same password or the same Apple/Google account, and keep their user id and data.
- API keys and the JWT secret. They are never copied. The app gets the environment's own keys (see below).
- Edge function source from the hosted project. The source platform's API cannot reliably hand back function source, so functions move from your local checkout instead: pass
--functions-dir supabase/functions(or run--functions-onlylater) and the CLI deploys each function, honouring_shared,deno.json/ import maps andverify_jwtinconfig.toml. The console import wizard does not move functions yet. - Vault secrets and function env vars (listed by name; re-enter with
scribase secrets set), custom auth hooks, SMTP, and auth provider settings (set them on the environment's Auth page),realtimepublications,pg_cronjobs, and column-level grants. - Keyless tables (no single-column primary key) are protected against duplicates on a resume, not on a second fresh run. Import into an empty environment.
Migrate a plain Postgres database (Neon, Render, RDS, self-hosted)
The generic counterpart to import supabase: a Neon branch, a Render or RDS instance, or any self-hosted Postgres database has no project ref, no service key, and no Auth/Storage/Edge Functions API alongside the database — just a connection string. Schema, data, RLS policies, grants, sequences and extensions move exactly the way they do from a Supabase source, with the same verification report at the end. No service key is asked for, and there is no storage or edge-function step, since a plain Postgres source has none of those.
scribase import postgres \
--connection-string <url> \
--org <scribase-org-id> \
--project <scribase-project-id> \
--env <scribase-env-id> \
[--schemas a,b] \
[--dry-run]Options
| Flag | Env override | Description |
|---|---|---|
--connection-string |
SCRIBASE_SOURCE_CONNECTION_STRING |
Source database URL |
--org |
— | Destination Scribase organization ID |
--project |
— | Destination Scribase project ID |
--env |
— | Destination Scribase environment ID |
--schemas a,b |
— | Copy only these schemas (default: every non-system schema) |
--dry-run |
— | Plan what would move without touching anything |
--resume <id> |
— | Continue an interrupted run from its last committed step |
--no-wait |
— | Start the import and print its import_id without following progress |
The connection string is a secret. Pass it via SCRIBASE_SOURCE_CONNECTION_STRING rather than on the command line in production contexts.
Example
A Neon database, migrated straight from its pooled connection string:
export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://neondb_owner:...@ep-cool-math-123456.us-east-2.aws.neon.tech/neondb?sslmode=require"
# Plan first — reads the source only, writes nothing
scribase import postgres \
--org acme --project app --env production --dry-run
# Remove --dry-run to run it. The server starts the import in the
# background and the command follows GET /v1/imports/{import_id},
# printing each phase, then the final verification report.
scribase import postgres \
--org acme --project app --env productionRender, RDS, or any other self-hosted Postgres works the same way — only the
connection string's host changes. The server does its own direct connection
to the destination environment, so this moves real rows today even though
public psql/pg_restore access into Scribase is still rolling out.
What is migrated
Extensions, schema (tables, types, functions, views, indexes, ENABLE ROW LEVEL SECURITY), grants on every schema/table/sequence/function, data (keyset-paged and idempotent), foreign keys and triggers, RLS policies, and a fail-closed verification report (per-table row counts and checksums, policy parity). See What is migrated above for the full step order — it is identical here, minus the auth-users and storage steps, which a plain Postgres database has no equivalent of.
What is not migrated
Nothing Supabase-specific applies (no auth.users, no storage.objects, no edge functions to redeploy), so the only carryovers from the Supabase list above are: API keys and the JWT secret (never copied — the app gets the environment's own keys), and pg_cron jobs, realtime publications, and column-level grants (not moved; recreate by hand).
Switch an Expo app to Scribase
The end-to-end procedure for a mobile app (Expo + supabase-js, email/password plus native Apple/Google sign-in):
Create the environment (console, or
scribase env create ...). Open its Storage page once so the storage service creates its schema.Dry run, then import:
Terminal export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://postgres:...@db.<ref>.supabase.co:5432/postgres" export SCRIBASE_SOURCE_SERVICE_KEY="<source service_role key>" scribase import supabase --project-ref <ref> --org <org> --project <app> --env production --dry-run scribase import supabase --project-ref <ref> --org <org> --project <app> --env productionProceed only when the verdict is
VERIFIED.Configure auth on the environment (Auth page), mirroring the source project's Auth settings:
site_url, the redirect allow list (myapp://**, plusexp://**for Expo Go), and for native sign-in the Apple provider (client id = the iOS bundle id, plus the Services ID if you also use the web flow) and the Google provider (client ids = the web, iOS and Android OAuth client ids, comma-separated). The console also asks for each provider's client secret.Print the app config:
Terminal scribase apps switch <org> <app> --env production # .env lines for Expo scribase apps switch <org> <app> --format json # for scripts scribase apps switch <org> <app> --key publishable # sb_publishable_ key instead of the anon JWTIt prints exactly
EXPO_PUBLIC_SUPABASE_URL=https://<label>.<domain>andEXPO_PUBLIC_SUPABASE_ANON_KEY=<key>, plus the auth setting names above. It never prints theservice_role/sb_secret_key. That key must never ship in an app, because it bypasses row-level security.Update the app: put the two values in
.env(or the EAS environment), rebuild, and ship.createClient(url, key)needs no code change.Cut over: rehearse steps 2 to 5 against a
stagingenvironment first. At cutover, stop writes on the source project (or force-update the old app version), import into the still-emptyproductionenvironment, and release the build that points at it. The URL and key printed in step 4 are fixed once the environment exists, so the build can be prepared before the final import. Existing users see the sign-in screen once.