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,
  "confirm": true
}
capydb import --source-url "postgres://..." --recreate --confirm --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).
  • The source must be a direct (or session-mode) endpoint: transaction-mode pooler URLs are rejected up front with an actionable error, because pg_dump needs session state that transaction pooling breaks. Concretely: Neon -pooler hostnames and Supabase's port-6543 pooler are refused; Neon direct endpoints and Supabase's session pooler (port 5432) work.
  • The importer is provider-aware: a Supabase source URL automatically excludes Supabase's platform-managed schemas and drops RLS policies that could not work outside Supabase - see Migrate from Supabase for exactly what that means.
  • 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://..."

Each check reports pass / warn / fail. Only a fail stops an import.

CheckReports
source_size_within_planFails when the source database is larger than the project plan's storage limit. Warns above 80%.
source_version_compatibleFails when the source runs a newer Postgres major than the database's selected target major - logical dumps flow forward, not backward. Targets can use Postgres 16, 17, or 18.
extensions_available_on_targetFails when the source uses extensions outside the target's allowlist, naming the offenders - and separating the ones nothing on the source depends on, which are safe to skip.
no_provider_auth_fk_couplingWarns when app tables have foreign keys into a provider-managed schema such as auth.users. A public-schema dump orphans them: the identity data has to move first.
no_provider_auth_policiesWarns when RLS policies call the provider's auth functions. Those policies are inert on plain Postgres and the import drops them - see Supabase RLS.
restore_plan_simulatedSimulates the restore-list filtering the import actually performs: which extensions get pre-created, which the restore cannot create (fails - pg_cron needs a preload and a restart, so enable it on the project first), and which event triggers are lost.
source_can_stream_changesWarns, never fails: whether a --follow import is possible from this source, and what to change if not. Names the provider's own remediation - see Supported sources. A plain import is unaffected.

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.

To grade a migration before you have a project - including the repository half, which no database probe can see - run a migration assessment.

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 node 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.

Choose the downtime model

A plain import is a point-in-time copy. Writes that land on the source after the import starts are not carried over. Its safe 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.

When that write freeze is unacceptable, use a follow import. It base-copies into a fresh, empty CapyDB project and then streams source changes through logical replication until cutover. The source must have logical replication enabled and writers must stop before the final cutover LSN is chosen.