CapyDB/ docs
GuidesImports & Migrations

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.

Before moving a production database it is reasonable to want three answers: what is actually in it, what will go wrong, and how long the application has to be down. capydb migrate scan answers all three, and it does so without an account, without installing anything beyond the CLI, and without your connection string leaving your machine.

capydb migrate scan --source-url "$OLD_DATABASE_URL" --out assessment.json

Drop the resulting file on capydb.dev/switch/check for the same report as a page. That page parses the file in your browser - it is never uploaded.

What it reads

Two halves, and the second one is the half nobody else has.

The database

Every probe is a plain SELECT against catalog and statistics views, in a session forced read-only with a statement timeout. No table data is read, nothing is counted with count(*), and any probe the connecting role cannot run is reported as skipped rather than failing the scan.

  • Who runs it. The provider, from the server's own catalogs rather than the hostname - see Supported sources for why the hostname is not good enough.
  • Whether it can stream. wal_level, the replication-slot budget, WAL senders, and whether the connecting role has REPLICATION. This decides whether the cutover window depends on the size of your data.
  • The physical inventory. Table sizes (with partitions summed into their parent), row estimates, index weight, tables over 100 GiB and 500 GiB, tables with no primary key, tables with no replica identity, foreign-key cycles, sequences near the end of their range, and indexes that have never been scanned or duplicate another index.
  • The provider coupling. Installed extensions with a dependent-object count, so an extension CapyDB does not offer is separated into "nothing uses this, drop it" and "this is load-bearing"; the live row-level-security corpus and how its policies resolve the caller; whether real users exist in a provider-managed auth schema; provider storage URLs persisted in data columns; and bookkeeping tables suggesting another data movement is already in flight.

The repository

Run from a project directory, the scan also reads your code - fully offline, nothing mutated:

  • Every database hostname your environment files reference, and which other repositories reference the same one. A database consumed by three services cannot be cut over by changing one service's environment variable, and nothing inside Postgres can tell you that. Pass --portfolio ../ to check sibling repositories.
  • Environment variables that resolve to different databases in different files. This is the silent failure: the framework loads .env.local while a script that pins .env loads the other one, so the application reads the new database while migrations still write to the old one, and nothing reports an error.
  • Which auth system and data-access layer the code uses, how many call sites are bound to a provider's client, and database functions the code calls through .rpc() that have no definition anywhere in the repository - those live only inside the provider's database.

What it produces

A verdict, its evidence, and a recommended path.

LevelMeaning
readyNothing here needs a decision before you start.
planningA handful of things are worth settling first.
assistedThere are blockers a self-service import will not resolve.

A scan run without --source-url reads only the repository. It can find real blockers there - a database with other consumers, an environment variable resolving to two different databases - but it never grades ready: "nothing to worry about" is not a conclusion you can draw from not having looked at the database.

With --project, a failing preflight check becomes a blocker and re-grades the verdict. That half is authoritative, so a failed restore simulation is never printed underneath a clean bill of health.

Findings are graded by what actually stops a migration, not by what sounds serious. Unused indexes are a note. An extension nothing depends on is a note, because the dump drops it. An extension with dependent objects is a blocker, because the schema will not restore. A source that cannot open a replication slot blocks only the streaming path, and is recorded as exactly that.

No invented durations

The report will not tell you the migration takes four hours. CapyDB has no measured copy rate for an arbitrary source over an arbitrary network, and a fabricated number is worse than none because people schedule maintenance windows around it.

What it reports instead is the volume that has to move, how much of that volume is index weight you could drop first, and - more usefully - whether the recommended path makes the cutover window depend on that volume at all. With a streaming import it does not.

Adding the control plane's verdict

Everything above grades your source against a set of rules. With a project, the scan can also carry the import preflight, which is not a rule table: the control plane connects to your source and simulates the actual restore against your actual target.

capydb migrate scan --source-url "$OLD_DATABASE_URL" --project my-app --out assessment.json

That adds which extensions get pre-created on the target, which the restore cannot create at all (pg_cron needs a preload and a restart, so it must be enabled on the project first), which event triggers are lost, and which foreign keys into a provider-managed schema a public-schema dump would orphan. These are the failures that otherwise arrive as a failed import job.

In CI

--out writes the same report as JSON, and --output json puts it on stdout, so a migration can be gated on it:

capydb migrate scan --source-url "$OLD_DATABASE_URL" --output json \
  | jq -e '.assessment.blockers | length == 0'

The grading lives in the CLI, not in a web page, so the terminal, the JSON and the page cannot disagree about whether a migration is safe.

Privacy

  • The --source-url connection is opened from your machine. It is not sent anywhere.
  • The report names schemas, tables, indexes, extensions and sampled column names from your database. Treat the file accordingly - the CLI writes it 0600.
  • The assessment page parses a dropped file in the browser. It performs no upload, and needs no account.
  • --project is the one flag that talks to CapyDB, because the preflight runs there.