Migrate from Neon
Move a Neon Postgres database into CapyDB.
Scan first
capydb migrate scan # read-only; no login neededThe scan classifies the repo (driver, auth, call sites, every database hostname in your env files)
and prints the migration plan with an effort grade. It flags the two Neon-specific code changes up
front: the @neondatabase/serverless driver swap and any db.batch() /
sql.transaction([...]) call sites that need rewriting to real transactions. If other repos share
the same database, pass --portfolio <dir> so the cutover list includes every consumer.
Swap the driver
@neondatabase/serverless speaks Neon's HTTP/WebSocket protocol and cannot connect to plain
Postgres. Swap to postgres-js:
capydb migrate deps # removes the Neon driver, adds "postgres" to package.jsonThen update the imports the scan listed:
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
// CapyDB's pooled endpoint (:6432) is transaction-mode PgBouncer:
// one connection per serverless instance, no prepared statements.
const client = postgres(process.env.DATABASE_URL!, { max: 1, prepare: false })
export const db = drizzle({ client })db.batch([...])(neon-http) becomesdb.transaction(async (tx) => { ... })- rebind every inner statement totx(adb-bound query inside amax: 1transaction deadlocks).- Raw
neon(url)`...`tagged-template call sites:postgres()is call-compatible, but hoist it to a module-level singleton - it holds a socket, so per-call construction leaks connections. - Point
drizzle.config.tsatDATABASE_DIRECT_URL(the:5432endpoint) - DDL through a transaction pooler is unreliable. - Drop
channel_binding=requirefrom stored URLs.
Get the source connection string
In the Neon console: Project → Dashboard → Connect (the "Connection string" panel). Take the direct connection string, not the -pooler one - a logical copy cannot run through a transaction-mode pooler, so preflight and import reject -pooler hostnames outright with an error telling you to switch.
It looks like:
postgres://user:password@ep-xxxx-xxxx.eu-central-1.aws.neon.tech/dbname?sslmode=requireNeon-specific caveats
- SNI: Neon routes connections by TLS SNI. CapyDB's importer uses standard TLS with SNI, so the plain hostname works. If you ever test the same URL with a very old client and get "endpoint could not be found", that is the SNI issue - irrelevant here, but it explains odd results from other tools.
- Autosuspend: a scaled-to-zero Neon endpoint takes a moment to wake; the preflight's connection timeout (10s) is usually enough, but retry once if the first preflight fails with a timeout.
- Branches: the URL identifies one branch's endpoint. Make sure it is the branch you actually want (usually
main). - Postgres major: check the project's Postgres version in Neon settings and create the CapyDB target on the same or a newer supported major (16, 17, or 18). A newer source cannot be imported into an older target.
Preflight
capydb import preflight --source-url "postgres://...neon.tech/dbname?sslmode=require"or dashboard → project → Import → paste URL → Run preflight. Fix anything that fails - size over plan limit, unsupported extensions (Neon projects often have pg_stat_statements enabled, which is on the allowlist; Neon-specific extensions are not commonly present in user databases).
Import
Stop or pause writers on the Neon side (the import is point-in-time; later writes are not carried over - see downtime honesty). Then:
capydb import --source-url "postgres://...neon.tech/dbname?sslmode=require" --recreate --wait--recreate drops and recreates the target database first, so re-running an import is safe and idempotent in outcome.
After the import
- Verify row counts with
SELECT count(*)per table - notpg_stat_user_tables.n_live_tup, which reads zero on a scaled-to-zero Neon source and false-alarms the comparison. - Verify sequences: spot-check
SELECT last_value FROM <seq>;for hot tables. \dx- confirm expected extensions exist on the CapyDB side;ANALYZE;once.- Cut over env vars:
capydb link --overwrite-envwritesDATABASE_URL(pooled) andDATABASE_DIRECT_URLin one step. Redeploy every consumer of the database at once. - Confirm nothing still talks to Neon before you decommission:
capydb migrate verify --source-url "postgres://...neon.tech/dbname?sslmode=require"Zero client connections on the old source means every consumer moved; anything listed is a deployable you missed. Keep the Neon project read-only for a rollback window (a week or two), then delete it.