case studies

Catch Hand Fixes on a Production Postgres Database With a Pinned Baseline

A shop runs its orders on Postgres. Migrations live in the repo and go out with each release. Then come the nights when they don’t: checkout is slow, someone adds an index on production by hand, someone else makes a column NOT NULL to stop bad rows, and the fixes work. Nobody writes them back into the migrations.

The default rolling baseline would report each of those changes once, in the next scan. That’s fine when someone reads every report. This team wanted more nagging than that. A hand fix should keep showing up until someone has decided about it, by writing the migration or by rolling the change back.

Four steps, from taking a pinned baseline to accepting or rolling back, next to a report with a type change and a new NOT NULL as breaking, an index added by hand as a warning, and no changes after accept.

Drift stays on the report until someone decides.

The setup

{
  "databases": [
    {"name": "prod", "driver": "postgres", "dsn_env": "PROD_DATABASE_URL", "baseline": "pinned"}
  ],
  "alerts": {"min_severity": "warning", "slack": {"url_env": "SLACK_WEBHOOK_URL"}}
}

The database user only needs to connect: RadarModel Schema reads the Postgres catalog inside a read-only transaction and never writes.

Three changes nobody wrote down

These are the released binary’s runs against a real Postgres 16 database, after three changes made by hand with psql: an index, a SET NOT NULL and a narrower email column.

$ radarmodel-schema check
RadarModel Schema: 2 breaking, 1 warning
prod  BREAKING  column public.customers.email      type text -> character varying(120)
prod  BREAKING  column public.orders.coupon        nullable yes -> no (inserts without it now fail)
prod  WARNING   index public.orders.orders_status  new

The next check says exactly the same, and so does every check after it. That’s the pinned baseline doing its job. check exits 1 each time, so a pipeline that runs it before a deploy refuses to ship code that was tested against a schema production no longer has.

The coupon line is the one that would have hurt. The app’s checkout code still inserts orders without a coupon, so the next release would have started failing orders the moment it shipped.

Once the team has written the migration that matches, one command takes the schema as it is now:

$ radarmodel-schema accept prod
accepted the current schema of prod as the baseline
$ radarmodel-schema check
RadarModel Schema: no changes
prod  ok  no changes (20 items)

Why schema drift starts has the background, and the quick start the setup.