scribasedocs

Control API /v1

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:

JSON
{
  "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.

JSON
{
  "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.

JSON
{
  "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:

Terminal
export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://..."
export SCRIBASE_SOURCE_SERVICE_KEY="eyJ..."

The CLI passes them to curl through stdin, never in process arguments.