Imports
The imports endpoint migrates a live Supabase project (or a Firebase or InstantDB export) into a Scribase environment. It reads the source project's schema, data, auth users, and storage objects and applies them to the destination environment.
Endpoint
POST /v1/imports
Request body:
{
"sourceProjectRef": "abcdefghijkl",
"sourceConnectionString": "postgresql://postgres:pass@db.abcdef.supabase.co:5432/postgres",
"sourceServiceKey": "eyJhbGci...",
"destination": {
"organizationId": "acme",
"projectId": "store",
"environmentId": "production"
},
"dryRun": false
}Set dryRun: true to receive a plan of what would be migrated without
touching anything. source selects the importer (supabase by default, or
firebase / instantdb with an export).
Optional fields:
| Field | Description |
|---|---|
sourceSchemas |
Array of schema names to copy. Default: every application schema (everything that is not a Supabase/Postgres system schema and not owned by an extension). |
includeStorage |
false skips buckets and objects (default true). |
targetStorageUrl + targetServiceKey |
The destination environment's storage API (https://<label>.<domain>/storage/v1) and its service_role key, used to upload object bytes. scribase import supabase fills both from the environment's API keys. Without them the control plane's SCRIBASE_TENANT_STORAGE_ENDPOINT / SCRIBASE_TENANT_SERVICE_KEY apply. |
verifyLogin |
{ "email" } for a structural login proof, { "email", "password" } for a live one. |
Headers: Idempotency-Key (optional; a retry with the same key returns the
run it already started instead of starting a second one).
Dry run — 200 OK: the plan.
{
"projectRef": "abcdefghijklmnopqrst",
"destination": "acme/store/production",
"planned": { "tables": 14, "rows": 82341, "authUsers": 1201, "objects": 421, "...": 0 },
"tables": ["public.orders", "public.customers"],
"findings": [{ "category": "edge_functions", "severity": "action", "detail": "..." }]
}Full run — 202 Accepted: the request is validated (a bad body or source
fails here with a 4xx, before anything is read), the import starts in the
background, and the response carries its import_id at once. The Location
header points at the progress route.
{
"import_id": "im-3f2a9c...",
"status": "running",
"phase": "schema",
"phases": ["schema", "auth_users", "data", "policies", "storage_objects", "edge_functions", "secrets", "verify", "done"],
"organization_id": "acme",
"project_id": "store",
"environment_id": "production",
"source": "supabase",
"source_ref": "abcdefghijklmnopqrst",
"counts": { "tables": 0, "rows": 0, "authUsers": 0, "objects": 0, "...": 0 },
"findings": 0,
"errors": [],
"resumable": false,
"attempts": 1,
"started_at": 1790000000,
"updated_at": 1790000000
}Each control-API replica runs at most SCRIBASE_IMPORT_MAX_RUNNING imports at
once (default 4); past that the route answers 429 import_capacity.
GET /v1/imports/{import_id}
The run's progress, in the same shape: status (running, succeeded,
failed, interrupted), the phase in progress, counts moved so far
(cumulative across resumes), the number of findings, errors
([{code, detail}]), resumable, attempts, and heartbeat_at (the running
replica's last liveness signal). Once it has succeeded the body adds passed
(the fail-closed parity verdict), the full report, and report_recorded,
which says whether the report was stored in the import history under the same
id (GET /v1/organizations/{org}/imports/{import_id}).
Progress is stored in the control-plane database (scribase.import_jobs,
migration 0017) after every committed step, so any replica answers a poll and
a restart loses nothing. Source credentials are never stored: they stay inside
the process running the import.
A run whose replica stopped (crash, restart, rollout) becomes interrupted
with an import_interrupted error: at control-API start for runs an earlier
process on the same host owned, and on any replica once the run's heartbeat is
older than SCRIBASE_IMPORT_STALE_SECONDS (default 120). The durable store uses
SCRIBASE_IMPORTS_REGISTRY_DATABASE_URL, falling back to
SCRIBASE_CONTROL_PLANE_ADMIN_DATABASE_URL; with neither set, progress is kept
in memory only and a restart loses in-flight runs.
POST /v1/imports/{import_id}/resume
Continues an interrupted run (or one that failed with a transient error)
from its last committed step. Completed phases are not re-run; every step
upserts on the destination, so the step in flight when the run stopped is
replayed safely. Send the original POST /v1/imports body again: the
credentials were never stored, and the destination and source must match the
run (409 import_resume_mismatch otherwise).
Tables without a single-column primary key have nothing to upsert on, so they are handled differently: before the first page of such a table is written, the importer records the destination table's row count in the checkpoint, and on every later page (including a replayed one) it skips the rows that already landed. The source is read in a total order (by the whole row) so a page holds the same rows on every read. A replayed page on a keyless table therefore does not insert duplicates, provided nothing else writes to that destination table while the import runs; if something does, the report carries a warning for that table.
| Answer | Meaning |
|---|---|
202 |
The run restarted; poll GET /v1/imports/{import_id} |
200 |
The run is already running (a retried resume never starts a second copy) |
409 import_finished |
It succeeded; nothing to resume |
409 import_not_resumable |
No step committed before it stopped, or it failed on an invalid request or checkpoint; start a new import |
The CLI (scribase import supabase ...) and the console wizard follow this
route automatically; --no-wait prints the started run and returns.
Report fields
The finished report carries:
| Field | Description |
|---|---|
moved |
What moved: tables, rows, auth users, identities, MFA factors, policies, buckets, objects, bytes |
findings |
Items that need an operator (edge functions to redeploy, secrets to re-enter) |
verdict |
VERIFIED, MISMATCH, or UNVERIFIED (fail-closed) |
verified |
true only when every parity check passed |
failures |
The checks that did not pass |
verification |
Per-check results and per-table source/target row counts and checksums |
Verification checks
After a full run, Scribase proves the copy before calling it done: per-table
row counts and checksums, RLS policy parity, auth-user and object counts, and
(when requested with verifyLogin) a sign-in for a known user.
Security
The source connection string and service key are secrets. Use environment variable overrides or a secret manager rather than hardcoding them in scripts:
export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://..."
export SCRIBASE_SOURCE_SERVICE_KEY="eyJ..."The CLI passes them to curl through stdin, never in process arguments.