zanith

migrate / verify · 01 — The shadow gate

A throwaway database that takes the hit first.

verify applies your pending migrations against a separate Postgres database, introspects the result, and diffs it against your declared schema. Anything missing is drift. The shadow is throwaway by design — destructive gates are off, no advisory lock is taken, nothing about the run is persisted.

declared.zanith filespending plansmigrations/*.tsrunnerapplyMigrations()shadow DBthrowawayintrospectpg_catalog → graphdiffdeclared vs livedrift reportok or [missing ops]generateloadapply (gates off)read backfeedemitreference
shadow throwawayno advisory lockdestructive gates offno preflightshadow.ts · v0.2

02 — The four stages

Reset, apply, introspect, diff. In that order.

One function does four things and returns a report. It never throws — the caller decides how to react to drift.

stage 01reset

wipe the shadow

Optional. Skipped when the shadow is already empty (CI service container, in-memory).

~ ~50ms
stage 02apply

run every pending migration

The same runner production uses, with the safety gates intentionally off — the shadow is throwaway.

~ depends on plan
stage 03introspect

read the result back

Walks pg_catalog. Rebuilds the same kind of schema graph the engine compiles queries through.

~ ~120ms
stage 04diff

compare to declared

Every operation needed to bring the shadow's schema into the declared shape becomes a drift line.

~ ~5ms

03 — Three shapes

Pick the cheapest that gives you isolation.

The shadow doesn't need to be a separate cluster — it just needs a Postgres connection that can be reset.

deviso · container

docker-compose'd

Two Postgres containers on different ports. Reset between verifies. Cheapest local setup.

ciiso · job

service container

GitHub or GitLab spawns a fresh Postgres per job. No reset hook needed — the env starts clean.

prodiso · schema

ephemeral schema

Same database server, isolated by search_path. Cheaper than a second cluster, fast to wipe.

04 — Four outcomes

Each verdict has a next step.

CI scripts can branch on the exit code. Logic failures (drift, apply-failed) and config failures (unreachable shadow) get different codes — different problems, different fixes.

ok · schemas match

exit 0

The shadow's resulting schema is identical to your declared schema. Re-run apply with the verified flag.

→ apply on prod

drift detected

exit 1

The shadow ran fine, but missing operations leave it diverging from declared. Regenerate or add the ops by hand.

→ fix migration

apply failed on shadow

exit 1

The migration itself errored. Up against prod would error the same way. Diagnose with the shadow inspector.

→ investigate

shadow unreachable

exit 2

Connection or auth issue with the shadow adapter. Different exit code from a logic failure.

→ check shadow config