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:
{
"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=requireis 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_dumpneeds session state that transaction pooling breaks. Concretely: Neon-poolerhostnames and Supabase's port-6543pooler are refused; Neon direct endpoints and Supabase's session pooler (port5432) 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: truedrops 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.completedfires 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.
| Check | Reports |
|---|---|
source_size_within_plan | Fails when the source database is larger than the project plan's storage limit. Warns above 80%. |
source_version_compatible | Fails 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_target | Fails 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_coupling | Warns 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_policies | Warns 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_simulated | Simulates 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_changes | Warns, 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 --waitThe 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 withpg_restorein a single transaction, with the same extension handling as URL imports. - Plain SQL (
pg_dumpwithout-Fc, or any.sqlfile). Replayed withpsqlin 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:
- Sequences - verify sequence values survived (they do in a normal logical dump, but check the tables that matter:
SELECT last_value FROM your_seq;). - Extensions - confirm
\dxlists what your app expects. ANALYZE- run it once so the planner has statistics for the freshly loaded data.- Env cutover - point
DATABASE_URL/DATABASE_URL_UNPOOLEDat 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:
- Run the preflight ahead of time and fix whatever it flags.
- Pick a quiet window. Put the app in maintenance mode or stop writers.
- Run the import with
--recreate --wait. - 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.
Postgres 18 features
What Postgres 18 adds that is worth using on CapyDB - time-sortable UUIDs, temporal constraints, virtual generated columns, and returning the old row - and how to write schemas that survive a major upgrade.
Migration assessment
Grade a migration before you commit to it - one read-only command against your database and your repository, and a report you can read in a terminal, in CI, or on a page.