Captures the `collections`-table DDL that historical scripts added to prod
but `migrations/1779853647564_initial-schema.js` did not capture:
- `visibility VARCHAR(20) DEFAULT 'private'` (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
- `tcg VARCHAR(50) DEFAULT 'MTG'` (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
- `tags TEXT` (scripts/add-collaboration-features.js, lines 15-21 — collections half only)
- `slug VARCHAR(100) UNIQUE` (scripts/add-collection-slugs.js, lines 17-20)
- `idx_collections_slug` UNIQUE INDEX (scripts/add-collection-slugs.js, line 79)
- `check_slug_format` CHECK constraint (scripts/add-collection-slugs.js, line 91)
- `image TEXT` (scripts/add-image-column.js, lines 14-17)
Idempotency (D2): every statement is `IF NOT EXISTS`-guarded
(ADD COLUMN IF NOT EXISTS, CREATE UNIQUE INDEX IF NOT EXISTS, plus a DO $$
pg_constraint guard for the CHECK since Postgres has no native IF NOT
EXISTS clause for named constraints). Safe against fresh, prod, and
re-apply.
Down() is a hard stub matching initial-schema style — these columns hold
visibility flags, slugs, tcg labels, tags, and images that production
collections rely on at every page render.
Out-of-scope per architect plan (B3 territory): collection_permissions,
collection_activity, users.is_pending, idx_collections_visibility, and
the 3 idx_collection_* indexes. Out-of-scope per architect plan (already
captured): is_system_collection (in 1780378340194).
Deferred DML: per-row slug backfill from `name` via
`lib/slug-utils.js::generateUniqueSlug`. Generating slugs on a fresh env
is moot (no pre-existing collections); operators of long-lived envs
already ran the backfill historically.
Static idempotency proof — grep confirms each B2 column/constraint is
defined exactly ONCE across all 8 existing migrations:
$ grep -nE "(visibility|tcg|^.*tags TEXT|slug VARCHAR|^.*image TEXT|check_slug_format|idx_collections_slug)" migrations/*.js
migrations/1781000000002_reconcile-collections-columns.js (sole owner)
The `image_url` / `stock_image_url` matches in `1779853647564_initial-schema.js`
are on the `cards` table, not `collections`. The `tags` table created in
`1781440721350_add-tagger-tables.js` is a separate table from this
migration's `collections.tags` column.
Verification: node --check ✅, npm run lint ✅ (0 errors, baseline 1
unrelated warning), npm run test:run ✅ (131/131). Live Neon-branch
verification deferred to operator runbook (D5 of the convoy plan).
Convoy: reconcile-historical-add-scripts
Brief: B2
Pre-assigned timestamp: 1781000000002
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: run migrations against Postgres service container in CI
Add a migrate job that applies node-pg-migrate against an ephemeral
Postgres 16 service container so broken migrations fail at PR time.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(migrations): retimestamp scan tables after initial-schema.
Fresh CI/postgres runs failed because 1748365200000 sorted before
initial-schema. Renamed to 1779853647566 with IF NOT EXISTS guards intact.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(migrations): ensure is_system_collection exists before description backfill.
Fresh CI Postgres runs initial-schema without this column (added historically
via scripts); ADD COLUMN IF NOT EXISTS makes the data migration safe on new
and existing envs.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Existing is_system_collection rows may carry stale copy; align them with VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION so UI matches new signups.
Co-authored-by: Cursor <cursoragent@cursor.com>
Upload confirmed scan frames to Vercel Blob and store the URL on user_cards
when routing to owned cards, completing the redesign-scanner-flow convoy.
Co-authored-by: Cursor <cursoragent@cursor.com>
Route Layer-2 identification through Vercel AI Gateway (AI_GATEWAY_API_KEY,
default google/gemini-2.5-flash-lite). Add Layer-1 browser Tesseract name-strip
OCR with pg_trgm fuzzy catalog match via /api/cards/identify-by-text before
escalating to vision.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(scanner): move card identification server-side (convoy #2)
Replace browser Gemini/OCR with POST /api/scan/identify, add card_submissions
review queue, remove user-writable cards INSERT, and surface disambiguation
when catalog matching is ambiguous.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: allowlist server-only lib/scan-gemini.js in LLM key gate
The scan pipeline helper lives under lib/ but is imported exclusively
from pages/api/scan/identify — exclude it from the client-side URL scan.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes P1 #11 of .convoys/ship-readiness.md (launch sequence step 7) —
"No migration tool — scripts/add-*.js graveyard". Schema changes
post-this-convoy ship as node-pg-migrate migrations under migrations/
at the repo root; the legacy 27 scripts/add-*.js / scripts/fix-*.js /
scripts/seed-*.js jobs remain append-only history per the no-go-zones
rule.
Decisions (full record in .convoys/migration-tool.md § Decisions):
D1 — Tool: node-pg-migrate@^8. Rejected drizzle-kit / prisma migrate /
kysely because each forces broader TypeScript surface than AGENTS.md
Gotcha #9 allows (TS is a devDep only). node-pg-migrate is
JavaScript-native, raw-SQL-friendly via pgm.sql(), and ESM-clean for
the post-bump-next-js "type": "module" repo. Brings pg@^8.21.0 as a
peer dep (dev-only; never loaded in the Next.js bundle).
D2 — Migrations directory: migrations/ at the repo root. Separates
the tool-wrapped artifacts from the historical scripts/migrations/
placeholder folder (which housed the lone pre-tool
2026-05-24-rename-admin-email.js migration and remains preserved for
the audit trail). Matches node-pg-migrate's default flag.
D3 — Tracking table: default pgmigrations (no name collision with
the existing 7-table bootstrap; zero CLI noise).
D4 — Backfill strategy: hand-translate scripts/setup-neon-db.js's
DDL into the initial migration verbatim. Each await sql`...` block
becomes one pgm.sql(`...`) call. Each CREATE uses IF NOT EXISTS, so
the migration is idempotent against fresh AND pre-existing envs —
re-running setup-db on an env that already has the schema is a no-op
DDL-wise (only records the pgmigrations row). Documented assumption:
prod has drifted via the 27 historical add-*.js scripts; reconciling
those into the migration history is the queued
reconcile-historical-add-scripts follow-up convoy.
D5 — Bootstrap reconciliation: split. setup-neon-db.js now (1)
validates ADMIN_INITIAL_PASSWORD + POSTGRES_URL, (2) spawns
`npm run migrate up` via child_process with stdio inherited, (3)
seeds the admin row with ON CONFLICT (email) DO NOTHING. The seven
DDL blocks are deleted from setup-neon-db.js; success/error message
copy is updated to mention the migration step explicitly.
D6 — CI integration: defer. Wiring a CI job that runs migrate up
against a test DB needs either a dedicated Neon branch + secret OR a
Postgres service container; both are real work. Surface as
wire-migrate-into-ci follow-up. Risk acknowledged in
.convoys/migration-tool.md § R3.
D7 — Down-migration on the initial backfill: hard stub. Rolling back
the initial schema would drop every user / card / collection / deck
row in the DB. The stub throws with a long-form error pointing at
the recommended alternative (branch the Neon database + forward-apply).
Future migrations that touch one of the seven bootstrap tables write
their own dated migration with a real down().
Verification (pre-PR):
- npm run lint → 128 problems (baseline preserved, zero regression;
migration file is lint-clean, no new ignore patterns)
- npm run test:run → 21/21 pass
- node --check on migrations/1779853647564_initial-schema.js + on
scripts/setup-neon-db.js → exit 0
- Module load + down() throw verified via dynamic import
- npm run migrate -- --help reaches the node-pg-migrate CLI through
the wrapper
Live verification against a Neon branch is deferred (no throwaway
branch available); the operator's optional post-merge sequence is
documented in .convoys/migration-tool.md § Operator runbook.
See .convoys/migration-tool.md § Follow-ups for the queued
wire-migrate-into-ci / reconcile-historical-add-scripts /
retire-graveyard-scripts-after-audit / audit-node-pg-migrate-transitive-deps
/ add-migration-template follow-up convoys.
Co-authored-by: Cursor <cursoragent@cursor.com>