CapyDB Docs
GuidesImports & Migrations

Imports

Move an existing Postgres database into CapyDB without rebuilding your app around a new platform.

How an import works

An import takes a source Postgres connection URL and loads that database into your CapyDB project as a job:

POST /v1/projects/{id}/imports
{
  "source_url": "postgres://user:password@source-host:5432/db?sslmode=require",
  "recreate": true
}
capydb import --source-url "postgres://..." --recreate --wait
  • The source must be publicly reachable — private/internal addresses are rejected. TLS is preferred automatically (sslmode=require is added unless the source explicitly opts out).
  • recreate: true drops and recreates the target database before importing, giving you a clean slate. Without it, the import loads into the existing database — fine for a first import into an empty project, asking for conflicts otherwise.
  • import.completed fires as a webhook event when the job finishes, and connected deployment integrations re-push env vars.

Preflight: find out before, not during

POST /v1/projects/{id}/imports/preflight (or the Import panel in the dashboard) connects to the source, inspects it, and grades the import without mutating anything:

capydb import preflight --source-url "postgres://..."

Three checks, each pass / warn / fail:

CheckFails when
source_size_within_planThe source database is larger than the project plan's storage limit. Warns above 80%.
source_version_compatibleThe source runs a newer Postgres major than the target (downgrades are not supported — logical dumps flow forward, not backward). Targets run Postgres 17.
extensions_available_on_targetThe source uses extensions outside the target's 13-extension allowlist (which includes pgvector and PostGIS) — the failing check names the offenders.

The worker re-runs the same gate before any destructive import step, so a source that drifted between preflight and import still gets caught before damage is done.

Importing a dump file

Some providers don't allow external connections to their Postgres instances (or only over a private network), so a connection-URL import can't reach them. For those, export a dump locally and upload it instead:

pg_dump -Fc --no-owner -d "postgres://...source..." -f app.dump
capydb import --file app.dump --recreate --wait

The CLI requests a one-hour presigned upload URL, streams the file directly to CapyDB's object storage (with a progress line on stderr), then starts the import referencing the uploaded object. The dashboard's Import panel offers the same flow: switch the source mode to Upload dump file, pick the file, and start the import once the upload finishes. Uploaded dumps are deleted from storage automatically after a successful import.

Two formats are accepted, detected automatically:

  • Custom format (pg_dump -Fc) — recommended. Restored with pg_restore in a single transaction, with the same extension handling as URL imports.
  • Plain SQL (pg_dump without -Fc, or any .sql file). Replayed with psql in a single transaction. Prefer -Fc: plain dumps that carry ownership or privilege statements for roles that don't exist on CapyDB will abort the import.

One caveat: dump-file imports validate size only. There is no live source to inspect, so the version/extension preflight doesn't apply — the upload is rejected up front if the dump file alone already exceeds your plan's storage limit, and because dumps are compressed, a dump that fits may still restore to a database that doesn't. The post-import storage check on the database host is the backstop; an import that overflows the plan limit fails and rolls back.

If you can connect to the source directly, prefer the connection-URL import — it gets the full preflight.

When to use imports

  • Move an existing app from another Postgres host (see the per-provider playbooks below)
  • Load a sanitized copy of an existing environment
  • Rebuild a project database from a known source

Migration playbooks

Provider-specific instructions, including where each provider hides the connection string:

Post-import checklist

Every playbook ends with the same four steps; they are worth internalizing:

  1. Sequences — verify sequence values survived (they do in a normal logical dump, but check the tables that matter: SELECT last_value FROM your_seq;).
  2. Extensions — confirm \dx lists what your app expects.
  3. ANALYZE — run it once so the planner has statistics for the freshly loaded data.
  4. Env cutover — point DATABASE_URL / DATABASE_URL_UNPOOLED at CapyDB, deploy, and watch the first minutes in Observability.

Downtime honesty

An import is a point-in-time copy. Writes that land on the source after the import starts are not carried over — logical replication for zero-downtime cutovers is not supported yet. The realistic low-downtime sequence is:

  1. Run the preflight ahead of time and fix whatever it flags.
  2. Pick a quiet window. Put the app in maintenance mode or stop writers.
  3. Run the import with --recreate --wait.
  4. Cut the env vars over and resume traffic.

For most databases under the plan size limits, the window is minutes, not hours.