Ships Brief 7 (documentation + verification runbook) of the
reconcile-historical-add-scripts convoy, ~2 months post-hoc. The
6 implementer briefs (B1-B6) landed 2026-06-14 to 2026-07-06 via
PRs #148, #149, #150, #151, #152, #153. This PR closes the loop:
- Brings the architect's parent convoy file + 6 brief files onto
main (they only existed on the stale convoy/reconcile-historical-
add-scripts branch, never merged)
- Adds § As-shipped to the parent convoy file documenting all 6
squash SHAs + PR numbers + merge dates + the reservation-timestamp
rename (1781000000001-006 → 1781442330001-006 in ec9bb2b, except
B3 which kept its original) + the B6 shipped-as-tiny-migration
deviation from the collapse-to-docs plan
- Fixes docs/SCHEMA_MAP.md § user_favorites (was stale
(user_id, card_id); actual polymorphic (item_type, item_id) per
B4's migration)
- Adds docs/MIGRATION_VERIFICATION_RUNBOOK.md — manual
fresh-Neon-branch vs prod pg_dump diff runbook per architect D5
- Flips .convoys/ship-readiness.md entries:
- reconcile-historical-add-scripts → RESOLVED
- retire-graveyard-scripts-after-audit → UNBLOCKED
- Adds two new queued follow-ups surfaced by the architect:
- unify-user-avatar-column (P3 — dual avatar column smell)
- drop-dead-cards-columns (P3 — cards.quantity + cards.favorited)
No source-code changes. Docs only.
Co-authored-by: Cursor <cursoragent@cursor.com>
4.8 KiB
Migration verification runbook
How to confirm that a fresh Neon branch's migrated schema matches a
long-lived environment (prod, preview, or similar). The migration history
under migrations/ is authoritative; this runbook is the manual check until
the queued wire-migrate-into-ci convoy automates it on every PR.
When to run
- After any reconcile-adjacent convoy lands on
main. - Before releasing schema changes to prod when migration correctness is uncertain.
- When suspecting drift between an env's actual schema and the migration history (rare).
Prerequisites
- A Neon account with permission to create and delete branches (or any other way to spin up a fresh Postgres DB on the same major version as prod).
POSTGRES_URLfor the branch under test — typically stored in a local.env.local.branchor passed inline (never commit branch URLs).ADMIN_INITIAL_PASSWORDset (required bynpm run setup-db; seeAGENTS.md§ 5).pg_dumpinstalled locally (PostgreSQL 16+), or available via a Docker container with network access to Neon.
Procedure
1. Create a throwaway Neon branch
neon branches create --name migrate-verify-YYYYMMDD --parent main
Or create an equivalent branch via the Neon console. Copy the branch
connection string into POSTGRES_URL.
2. Onboard the fresh branch via setup-db
POSTGRES_URL=<branch-url> \
ADMIN_INITIAL_PASSWORD=$(openssl rand -base64 24) \
npm run setup-db
setup-db runs npm run migrate up (every file in migrations/ in
timestamp order) and seeds the admin user.
3. Snapshot the fresh branch schema
POSTGRES_URL=<branch-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > fresh-schema.sql
4. Snapshot prod schema (read-only)
POSTGRES_URL=<prod-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > prod-schema.sql
Use a read-only role or connection if your operator policy requires it. Do not run DDL against prod during verification.
5. Diff
diff -u prod-schema.sql fresh-schema.sql
- Empty output — structural parity confirmed for tables, columns,
constraints, and indexes in
public. - Non-empty output — investigate before releasing; see below.
6. Delete the throwaway branch
neon branches delete migrate-verify-YYYYMMDD
Known-safe diffs
These differences from pg_dump are not real schema drift:
- Schema owner / comment lines that differ between Neon projects.
- Extension version pins (
CREATE EXTENSIONversion strings). pgmigrationsrow content — the fresh env accumulates rows as migrations apply; prod may have the same rows with different apply timestamps.- Constraint or index name differences when prod objects were created
by historical
scripts/add-*jobs with autogenerated names and fresh envs use migration-defined names (material shape must still match). - Column order within a table (historical ALTER order vs migration order).
When drift is detected
- Halt — do not release until the delta is understood.
- Identify which env is canonical. Usually prod. If prod is missing
a column that exists in the migration history, run
POSTGRES_URL=<prod-url> npm run migrate upto catch up. - If prod has something not in the migration history, file a new
convoy or brief describing the delta and ship a reconciliation
migration following the pattern in
.convoys/reconcile-historical-add-scripts/. - Never edit existing migrations —
pgmigrationspins applied files. Always add a new dated migration to correct schema.
Supplementary spot-checks
For high-risk tables, compare column / constraint / index counts between envs:
SELECT table_name, column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
SELECT table_name, constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'public'
ORDER BY table_name, constraint_name;
SELECT tablename, indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
A mismatch in column count, constraint count, or index count is material drift.
Automation path
The queued wire-migrate-into-ci convoy (see .convoys/ship-readiness.md
§ Queued convoys) will run npm run migrate up against a test DB on every
PR. Until that ships, this operator runbook is the only automated-parity
signal.
Cross-references
AGENTS.md§ 4 Gotcha #6 — migration tool adoption history..convoys/migration-tool.md—node-pg-migrateadoption convoy..convoys/reconcile-historical-add-scripts.md§ As-shipped — the convoy that folded historical add-script DDL intomigrations/..cursor/rules/db-and-schema.mdc— schema-change conventions.