Skip to content

Imports

Start, follow and resume an import into one environment of a project (see Importing an app for what comes across). The CLI’s flatstack import and the MCP tools import_project and get_import_status use these routes.

An import writes your app users’ data, so starting or resuming one needs data:write (owners, admins, and tokens with that scope); following one needs data:read.

Request Needs What it does
POST /v1/projects/{projectId}/imports data:write Starts an import, or with dryRun: true returns its report and writes nothing.
GET /v1/projects/{projectId}/imports data:read { imports }: the latest 20, newest first.
GET /v1/projects/{projectId}/imports/{importId} data:read The import’s status and progress.
GET /v1/projects/{projectId}/imports/{importId}/report data:read { status, report, files }. ?format=markdown returns the migration report alone.
POST /v1/projects/{projectId}/imports/{importId}/resume data:write Runs the next step. A completed import is returned as it is.

Starting

{
  "source": "supabase",
  "environment": "production",
  "dryRun": true,
  "credentials": {
    "databaseUrl": "postgresql://postgres.abcd:password@aws-0-eu-west-2.pooler.supabase.com:5432/postgres",
    "projectUrl": "https://abcd.supabase.co",
    "serviceRoleKey": "eyJ...",
    "s3": { "accessKeyId": "...", "secretAccessKey": "...", "region": "eu-west-2" }
  },
  "functions": [{ "name": "send-digest", "files": [{ "path": "index.ts", "content": "..." }] }]
}
  • source: supabase, lovable or clerk (below). environment defaults to production.
  • credentials.databaseUrl is required; projectUrl with serviceRoleKey, or s3, lets the import copy files.
  • functions: the edge functions in your repo’s supabase/functions/, copied to functions/legacy/.

A dry run answers 200 with { dryRun: true, engine, blocked, summary, issues, report, files }. files is everything the import writes to your repo, MIGRATION_REPORT.md included; the real run’s report is the same, word for word.

A real run answers 201 with the import once its first step finished or failed, or 202 while it’s still running. It copies for about 20 seconds per request.

{
  "dryRun": false,
  "id": "imp_…",
  "source": "supabase",
  "environment": "production",
  "engine": "d1",
  "status": "running",
  "phase": "data",
  "position": "copying table todos",
  "progress": { "users": 4, "usersSkipped": 0, "signInLinks": 4, "rows": { "profiles": 3 }, "objects": 0, "objectsSkipped": 0, "bytes": 0 },
  "summary": { "tables": 4, "rows": 10, "users": 4, "objects": 2 },
  "review": 14,
  "error": null,
  "canResume": true
}

status is running, completed or failed. phase is schema, users, data, foreign_keys, files or done. A failed import has error: { code, message }; resuming carries on from where it stopped. usersSkipped counts users whose email address already belonged to someone in the project; they aren’t imported.

Clerk

{
  "source": "clerk",
  "dryRun": true,
  "credentials": {
    "secretKey": "sk_live_...",
    "passwords": [{ "userId": "user_2abc", "digest": "$2b$10$...", "hasher": "bcrypt" }],
    "webhooks": ["https://example.com/api/clerk-webhook"]
  }
}
  • credentials.secretKey is required: the Clerk application’s secret key.
  • passwords: the id, password_digest and password_hasher columns of the user export CSV from Clerk’s dashboard. Only bcrypt hashes are kept. flatstack import clerk --export <file.csv> reads the file for you.
  • webhooks: your Clerk webhook endpoints, for the report.

A Clerk import’s report file is MIGRATION_REPORT_CLERK.md. Its phase is schema, users, owners, organizations, memberships or done, and its progress is { users, usersMerged, usersSkipped, signInLinks, ownerIdsRewritten, organizations, memberships }. In the owners phase the import changes merged users’ Clerk IDs (user_…) to their Flatstack IDs in your own tables’ text columns; ownerIdsRewritten counts the rows changed, and the report lists each column. usersMerged counts Clerk users merged into a user already in the project with the same verified email; usersSkipped counts those whose matching address was unverified on one side, who aren’t imported.

Errors

Status Code When
400 validation_failed The body doesn’t match. Credential values are never echoed back.
409 import_running Another import into the environment is running, or another request is running this one’s step.
409 import_expired The import failed more than a day ago, so its credentials were deleted. Start it again; nothing is copied twice.
422 import_blocked Something stops the import, listed in details.problems: a table that already exists, or a column D1 can’t hold. Nothing was written.
502 import_source_unreachable The source database couldn’t be read. details.error has the reason, with credentials removed.

Starting, planning, resuming, finishing and failing are written to the audit log (import.started, import.planned, import.resumed, import.completed, import.failed), without credentials. A project in another account, or an environment a deploy token wasn’t made for, answers 404 not_found.

As Markdown: api/imports.md