# 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:

```sh
export SCRIBASE_SOURCE_CONNECTION_STRING="postgresql://..."
export SCRIBASE_SOURCE_SERVICE_KEY="eyJ..."
```

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