DocsAdministrationCardinal 0.7.0

Upgrades

Migrations are expand-only, so rolling back is deploying the old image and nothing else.

llms.txt

Migrations are expand-only: they never drop, rename, retype, narrow, or add NOT NULL without a default. A test enforces it rather than review. The previous build therefore keeps working against a newer schema, which is what makes rolling back "deploy the old image" and nothing else.

That last sentence is now demonstrated rather than argued. make verify-rollback in the repository migrates a database with the current build, then starts the published 0.1.0 and 0.2.0 images against it and requires them to serve. It checks the failure direction too: mark a migration breaking and the older build refuses to start, which is the behaviour the guarantee depends on.

What it does not cover is every query an older build might make against newer columns: it exercises startup and health. Expand-only is what makes the rest safe, and that is enforced per migration.

cardinal-server migrate            # a separate step, not a side effect of starting
cardinal-server migrate -status

Applying the schema while other replicas still serve the old one is how rolling deploys break, so it is a decision rather than something that happens.

At startup Cardinal checks: schema behind means refuse and tell you to migrate, schema ahead means warn and serve, and a migration that declared itself breaking means refuse: an older build can tell, because the reason is written into schema_migrations when applied.

Partitions are created ahead, and reported

The journal and the decision log are partitioned by time, and the partitions are created by migration rather than appearing on their own. The schema ships them to the end of 2035, with a DEFAULT partition behind them so running out slows things down instead of stopping them.

The server warns at startup when a table has under two years of partitions left, and warns differently once rows are landing in the backstop, which is the point at which adding the proper partition means moving them first.

Do not drop those partitions to "undo" a migration. Every mutation writes its journal entry in the same transaction as the change it records, so a row that routes nowhere fails the change with it.

Moving between PostgreSQL releases

Cardinal needs PostgreSQL 18 or newer. Releases up to 0.4.0 required 19 and ran on its betas. Moving the database to another release is not a restart: a major release, or a 19 beta, cannot open a data directory another one created. The database moves by dump and restore:

# with the old PostgreSQL still running, and Cardinal stopped
pg_dump -Fc -d cardinal > cardinal.pgc
# start the new PostgreSQL on an empty data directory, then
pg_restore --exit-on-error -d cardinal cardinal.pgc
cardinal-server migrate   # from the build you are moving to
cardinal audit verify     # the journal's chain, end to end

The audit journal survives the move, and a test checks it: it writes a journal, dumps it, restores it into a second PostgreSQL and requires the hash chain to verify and end on the same hash. It held from a 19 beta to 18 and from 18 to a 19 beta. cardinal audit verify is the same check against your own journal, and the one to run after a move.

Rolling back across the move means moving the database back: releases up to 0.4.0 refuse to start on anything older than 19.

Before you upgrade

Read the changelog. It is written by hand precisely so that "this one is a security fix" is something it can say.