deckhearth/README.md

198 lines
7.4 KiB
Markdown
Raw Permalink Normal View History

feat(brand): unify on Deck Hearth across in-repo strings + infra (P1 brand decision) Resolves the launch-blocking 'TCG Vault vs Deck Hearth' inconsistency called out in AGENTS.md line 5 since project setup. Operator gate-0 decision: Deck Hearth wins. Two briefs applied serially. B1 (mechanical): 7-file display + comment sweep. B2 (infrastructure): Redis prefix rename in lib/rate-limit.js (5 prefixes, accept one-time counter reset), package.json + lockfile regen (STOP-on-churn confirmed only name lines changed), admin/alice/bob email rename in seed scripts + login pre-fill + NEW idempotent migration script scripts/migrations/2026-05-24-rename-admin-email.js. Risk 4 PRESERVE applied: test/lib/permission-middleware.test.js retains admin@tcgvault.com literal with 7-line architect-authored why comment (documents pre-fix-auth-bypass bug shape; preserves historical truth per project's gotcha-documentation convention). All 5 D-decisions ratified at gate-1 (Deck Hearth / deck-hearth / deckhearth / admin@deckhearth.com / full deckhearth Redis prefix). Local: lint 128 baseline (B1 + B2), vitest 21/21 (B1 + B2). CI all green: Playwright smoke 3/3 against rebranded preview in 1m4s, forbidden-cors-headers pass, forbidden-endpoints pass, Screenshot diff pass, Vercel deployment complete. Cross-validation lineage: 4th convoy where the same 3-test smoke spec defends auth surface through sweeping change (after PR #15 Layout default-user, PR #19 CORS, PR #20 rate-limit, now this PR #21 brand rename). OPERATOR POST-MERGE ACTION REQUIRED: run 'node scripts/migrations/2026-05-24-rename-admin-email.js' against prod Neon DB before next admin login (ordering: migration FIRST, then any subsequent setup-db invocation). Migration is ESM, idempotent, UNIQUE-collision-safe. PR #21 architect-commit 50ce9ab, B1 ac8c998, B2 1c18d21.
2026-05-25 03:28:29 -04:00
# Deck Hearth
A modern trading card game collection manager built with Next.js and Neon Database.
## 🚀 Features
- **Card Management**: Track your MTG, Pokémon, and Lorcana cards
- **Collection Organization**: Create and manage card collections
- **Deck Building**: Build and share decks
- **Authentication**: Secure user accounts with JWT
- **Admin Panel**: Manage cards and users
- **Real-time Pricing**: Track card values
## 🛠️ Tech Stack
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
- **Frontend**: Next.js 16 (Pages router), React 18, JavaScript (TypeScript is a devDep only — see `AGENTS.md` Gotcha #9)
- **Backend**: Next.js API Routes
- **Database**: Neon PostgreSQL (serverless)
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
- **Authentication**: JWT with bcrypt (24-hour expiry; `lib/auth-secret.js` is the single source of truth for `JWT_SECRET`)
- **Rate limiting**: `@upstash/ratelimit` on `/api/auth/login` + `/api/auth/register` (5 attempts / 15 min per IP)
- **Testing**: Vitest (unit); Playwright queued
- **Styling**: Tailwind CSS
- **Deployment**: Vercel
## 📦 Installation
1. **Clone the repository**
```bash
git clone <repository-url>
cd tcg-vault
```
2. **Install dependencies**
```bash
npm install
```
3. **Set up environment variables**
```bash
cp .env.example .env.local
```
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
Update `.env.local` with your Neon database URL and a real JWT secret:
```env
POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database"
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
JWT_SECRET="<generate with: openssl rand -hex 32>"
feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README Closes P0 #3 from .convoys/ship-readiness.md. scripts/setup-neon-db.js: - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase() before any DB connection. Fail loudly (process.exit(1)) with an actionable message if unset or empty. - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12). - Delete the two console.log lines that echoed admin user + password to stdout (R3 - stdout leak into CI logs). - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db on an env with the admin row already present is a no-op for the password (R4 - silent rotation prevention). Rotation of existing weak-hash admin rows is out of scope (Decision A - queued for the rotate-default-admin follow-up convoy). README.md: - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN for completeness; they're optional for local dev). - Replace the "Default Admin Account" section with "First-time admin setup", documenting the env var, openssl rand suggestion, and the operator rotation note for envs that predate this change. - Zero occurrences of 'admin123' remain in README.md (the operator rotation note refers to "the prior weak default" instead of naming the literal string, so grep verification A2 holds). Decisions A1 (going-forward only), B (operational change allowed), C1 (no vitest coverage - manual smoke in PR description) per .convoys/drop-public-setup.md section Decisions. Smoke output: see PR description. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:57:31 -04:00
# Required for `npm run setup-db` — used once to hash the initial admin password.
# Set in .env.local for local dev, or as a CI secret if you run setup from CI.
ADMIN_INITIAL_PASSWORD="<generate with: openssl rand -base64 24>"
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
# Optional — exercise the rate limiter locally. Without them, `lib/rate-limit.js`
# warn-and-no-ops in dev. In production these are auto-provisioned by the
# Vercel Upstash Marketplace integration.
KV_REST_API_URL="https://<your-upstash-host>.upstash.io"
KV_REST_API_TOKEN="<your-upstash-rest-token>"
```
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
`JWT_SECRET` is **required**`lib/auth-secret.js` throws at import time if it's unset.
feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README Closes P0 #3 from .convoys/ship-readiness.md. scripts/setup-neon-db.js: - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase() before any DB connection. Fail loudly (process.exit(1)) with an actionable message if unset or empty. - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12). - Delete the two console.log lines that echoed admin user + password to stdout (R3 - stdout leak into CI logs). - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db on an env with the admin row already present is a no-op for the password (R4 - silent rotation prevention). Rotation of existing weak-hash admin rows is out of scope (Decision A - queued for the rotate-default-admin follow-up convoy). README.md: - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN for completeness; they're optional for local dev). - Replace the "Default Admin Account" section with "First-time admin setup", documenting the env var, openssl rand suggestion, and the operator rotation note for envs that predate this change. - Zero occurrences of 'admin123' remain in README.md (the operator rotation note refers to "the prior weak default" instead of naming the literal string, so grep verification A2 holds). Decisions A1 (going-forward only), B (operational change allowed), C1 (no vitest coverage - manual smoke in PR description) per .convoys/drop-public-setup.md section Decisions. Smoke output: see PR description. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:57:31 -04:00
`ADMIN_INITIAL_PASSWORD` is **required** for `npm run setup-db` — the script exits with code 1 if it's unset.
4. **Set up the database**
```bash
npm run setup-db
```
feat(infra): adopt node-pg-migrate + backfill initial schema migration (#32) 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>
2026-05-27 00:01:58 -04:00
This applies every pending migration under `migrations/` (via
[`node-pg-migrate`](https://github.com/salsita/node-pg-migrate)) and
then seeds the initial admin user. To run just the migration step
without seeding, use `npm run migrate up`.
5. **Start development server**
```bash
npm run dev
```
feat(infra): adopt node-pg-migrate + backfill initial schema migration (#32) 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>
2026-05-27 00:01:58 -04:00
## 🧱 Schema changes (post-`migration-tool` convoy)
Schema is now managed by `node-pg-migrate`. New migrations live under
`migrations/` at the repo root (the legacy `scripts/migrations/`
placeholder is preserved for the one pre-existing dated script and is
not used going forward).
```bash
# 1. Generate a new migration file (JS template, timestamp-prefixed)
npm run migrate create add-foo-column -- -j js
# 2. Edit the generated file under migrations/<timestamp>_add-foo-column.js
# Put your DDL in up(); write a real down() if rollback is safe.
# 3. Apply it locally (POSTGRES_URL from .env.local)
npm run migrate up
# 4. Commit the migration file + any docs/SCHEMA_MAP.md updates together.
```
Onboarding a new env is now exactly `npm install` → seed `.env.local`
`npm run setup-db` (which chains `npm run migrate up` and then
seeds the admin user).
> **Do not edit historical `scripts/add-*.js` / `scripts/fix-*.js` /
> `scripts/seed-*.js`** — those are already-run, append-only jobs.
> They are preserved for the audit trail; new column / constraint
> work goes through a migration file instead.
## 🗄️ Database Schema
The application uses the following tables:
- `users` - User accounts and authentication
- `cards` - Card information and metadata
- `user_cards` - User's card collections
- `collections` - Named card collections
- `collection_cards` - Cards in collections
- `decks` - Deck definitions
- `deck_cards` - Cards in decks
## 🔧 API Endpoints
### Authentication
- `POST /api/auth/register` - User registration
- `POST /api/auth/login` - User login
### Admin
- `GET /api/admin` - Admin panel data
### Health Check
- `GET /api/health` - Application health
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
> **Note:** Earlier revisions of this README also listed `GET /api/test-db` (and three other unauthenticated dev endpoints: `/api/simple`, `/api/test-auth`, `/api/setup-database`). All four were deleted in `fix-auth-bypass` Brief 3 (commit `fc0dd73`) and CI now blocks their reintroduction. Don't recreate them.
## 🚀 Deployment
This app is configured for deployment on Vercel:
1. **Connect your repository** to Vercel
2. **Set environment variables** in Vercel dashboard
3. **Deploy automatically** on push to main branch
## 📁 Project Structure
```
tcg-vault/
├── pages/ # Next.js pages and API routes
│ ├── api/ # API endpoints
│ │ ├── auth/ # Authentication routes
│ │ └── admin/ # Admin routes
│ ├── _app.js # App wrapper
│ └── index.js # Home page
├── lib/ # Utility libraries
│ └── database.js # Database adapter
├── scripts/ # Database setup scripts
├── public/ # Static assets
└── .env.local # Environment variables
```
feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README Closes P0 #3 from .convoys/ship-readiness.md. scripts/setup-neon-db.js: - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase() before any DB connection. Fail loudly (process.exit(1)) with an actionable message if unset or empty. - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12). - Delete the two console.log lines that echoed admin user + password to stdout (R3 - stdout leak into CI logs). - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db on an env with the admin row already present is a no-op for the password (R4 - silent rotation prevention). Rotation of existing weak-hash admin rows is out of scope (Decision A - queued for the rotate-default-admin follow-up convoy). README.md: - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN for completeness; they're optional for local dev). - Replace the "Default Admin Account" section with "First-time admin setup", documenting the env var, openssl rand suggestion, and the operator rotation note for envs that predate this change. - Zero occurrences of 'admin123' remain in README.md (the operator rotation note refers to "the prior weak default" instead of naming the literal string, so grep verification A2 holds). Decisions A1 (going-forward only), B (operational change allowed), C1 (no vitest coverage - manual smoke in PR description) per .convoys/drop-public-setup.md section Decisions. Smoke output: see PR description. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:57:31 -04:00
## 🔐 First-time admin setup
feat(infra): adopt node-pg-migrate + backfill initial schema migration (#32) 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>
2026-05-27 00:01:58 -04:00
`npm run setup-db` first applies every pending migration (via `npm run
migrate up`), then creates a single admin user. The password is read
from the `ADMIN_INITIAL_PASSWORD` environment variable; the script
exits with code 1 (and does not open a database connection) if the
feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README Closes P0 #3 from .convoys/ship-readiness.md. scripts/setup-neon-db.js: - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase() before any DB connection. Fail loudly (process.exit(1)) with an actionable message if unset or empty. - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12). - Delete the two console.log lines that echoed admin user + password to stdout (R3 - stdout leak into CI logs). - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db on an env with the admin row already present is a no-op for the password (R4 - silent rotation prevention). Rotation of existing weak-hash admin rows is out of scope (Decision A - queued for the rotate-default-admin follow-up convoy). README.md: - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN for completeness; they're optional for local dev). - Replace the "Default Admin Account" section with "First-time admin setup", documenting the env var, openssl rand suggestion, and the operator rotation note for envs that predate this change. - Zero occurrences of 'admin123' remain in README.md (the operator rotation note refers to "the prior weak default" instead of naming the literal string, so grep verification A2 holds). Decisions A1 (going-forward only), B (operational change allowed), C1 (no vitest coverage - manual smoke in PR description) per .convoys/drop-public-setup.md section Decisions. Smoke output: see PR description. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:57:31 -04:00
variable is unset or empty.
- **Local dev:** set `ADMIN_INITIAL_PASSWORD` in `.env.local` before running
`npm run setup-db`. Use `openssl rand -base64 24` (or any other strong
source) to generate the value.
- **CI / Vercel:** set `ADMIN_INITIAL_PASSWORD` as a project secret if setup
ever runs from CI. The env var is **only** read by the seed script; runtime
auth uses the per-user password stored in the database.
feat(brand): unify on Deck Hearth across in-repo strings + infra (P1 brand decision) Resolves the launch-blocking 'TCG Vault vs Deck Hearth' inconsistency called out in AGENTS.md line 5 since project setup. Operator gate-0 decision: Deck Hearth wins. Two briefs applied serially. B1 (mechanical): 7-file display + comment sweep. B2 (infrastructure): Redis prefix rename in lib/rate-limit.js (5 prefixes, accept one-time counter reset), package.json + lockfile regen (STOP-on-churn confirmed only name lines changed), admin/alice/bob email rename in seed scripts + login pre-fill + NEW idempotent migration script scripts/migrations/2026-05-24-rename-admin-email.js. Risk 4 PRESERVE applied: test/lib/permission-middleware.test.js retains admin@tcgvault.com literal with 7-line architect-authored why comment (documents pre-fix-auth-bypass bug shape; preserves historical truth per project's gotcha-documentation convention). All 5 D-decisions ratified at gate-1 (Deck Hearth / deck-hearth / deckhearth / admin@deckhearth.com / full deckhearth Redis prefix). Local: lint 128 baseline (B1 + B2), vitest 21/21 (B1 + B2). CI all green: Playwright smoke 3/3 against rebranded preview in 1m4s, forbidden-cors-headers pass, forbidden-endpoints pass, Screenshot diff pass, Vercel deployment complete. Cross-validation lineage: 4th convoy where the same 3-test smoke spec defends auth surface through sweeping change (after PR #15 Layout default-user, PR #19 CORS, PR #20 rate-limit, now this PR #21 brand rename). OPERATOR POST-MERGE ACTION REQUIRED: run 'node scripts/migrations/2026-05-24-rename-admin-email.js' against prod Neon DB before next admin login (ordering: migration FIRST, then any subsequent setup-db invocation). Migration is ESM, idempotent, UNIQUE-collision-safe. PR #21 architect-commit 50ce9ab, B1 ac8c998, B2 1c18d21.
2026-05-25 03:28:29 -04:00
- **Admin email:** the seed creates `admin@deckhearth.com`. Change the password
feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README Closes P0 #3 from .convoys/ship-readiness.md. scripts/setup-neon-db.js: - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase() before any DB connection. Fail loudly (process.exit(1)) with an actionable message if unset or empty. - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12). - Delete the two console.log lines that echoed admin user + password to stdout (R3 - stdout leak into CI logs). - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db on an env with the admin row already present is a no-op for the password (R4 - silent rotation prevention). Rotation of existing weak-hash admin rows is out of scope (Decision A - queued for the rotate-default-admin follow-up convoy). README.md: - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN for completeness; they're optional for local dev). - Replace the "Default Admin Account" section with "First-time admin setup", documenting the env var, openssl rand suggestion, and the operator rotation note for envs that predate this change. - Zero occurrences of 'admin123' remain in README.md (the operator rotation note refers to "the prior weak default" instead of naming the literal string, so grep verification A2 holds). Decisions A1 (going-forward only), B (operational change allowed), C1 (no vitest coverage - manual smoke in PR description) per .convoys/drop-public-setup.md section Decisions. Smoke output: see PR description. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:57:31 -04:00
immediately after first login via the app's profile settings.
> **Operators of envs that pre-date this change:** `npm run setup-db` is
> idempotent (`ON CONFLICT (email) DO NOTHING`) — re-running it with
> `ADMIN_INITIAL_PASSWORD` set will **not** rotate an existing admin row's
> password. If your environment was set up before this change and still has
> the prior weak default, rotate the password manually via the app
> after logging in, or wait for the queued `rotate-default-admin` follow-up
> convoy.
feat(brand): unify on Deck Hearth across in-repo strings + infra (P1 brand decision) Resolves the launch-blocking 'TCG Vault vs Deck Hearth' inconsistency called out in AGENTS.md line 5 since project setup. Operator gate-0 decision: Deck Hearth wins. Two briefs applied serially. B1 (mechanical): 7-file display + comment sweep. B2 (infrastructure): Redis prefix rename in lib/rate-limit.js (5 prefixes, accept one-time counter reset), package.json + lockfile regen (STOP-on-churn confirmed only name lines changed), admin/alice/bob email rename in seed scripts + login pre-fill + NEW idempotent migration script scripts/migrations/2026-05-24-rename-admin-email.js. Risk 4 PRESERVE applied: test/lib/permission-middleware.test.js retains admin@tcgvault.com literal with 7-line architect-authored why comment (documents pre-fix-auth-bypass bug shape; preserves historical truth per project's gotcha-documentation convention). All 5 D-decisions ratified at gate-1 (Deck Hearth / deck-hearth / deckhearth / admin@deckhearth.com / full deckhearth Redis prefix). Local: lint 128 baseline (B1 + B2), vitest 21/21 (B1 + B2). CI all green: Playwright smoke 3/3 against rebranded preview in 1m4s, forbidden-cors-headers pass, forbidden-endpoints pass, Screenshot diff pass, Vercel deployment complete. Cross-validation lineage: 4th convoy where the same 3-test smoke spec defends auth surface through sweeping change (after PR #15 Layout default-user, PR #19 CORS, PR #20 rate-limit, now this PR #21 brand rename). OPERATOR POST-MERGE ACTION REQUIRED: run 'node scripts/migrations/2026-05-24-rename-admin-email.js' against prod Neon DB before next admin login (ordering: migration FIRST, then any subsequent setup-db invocation). Migration is ESM, idempotent, UNIQUE-collision-safe. PR #21 architect-commit 50ce9ab, B1 ac8c998, B2 1c18d21.
2026-05-25 03:28:29 -04:00
> **Operators of envs that pre-date the `pick-a-name` convoy (2026-05-24):**
> the admin row was renamed from `admin@tcgvault.com` to
> `admin@deckhearth.com`. Run
> `node scripts/migrations/2026-05-24-rename-admin-email.js` once after
> deploy to UPDATE any existing `@tcgvault.com` user rows (the admin row,
> plus alice/bob if `npm run create-test-users` was ever run). Re-running
> the migration after the first run is idempotent and prints "Nothing to
> migrate." Verify post-migration with
> `psql $POSTGRES_URL -c "SELECT email FROM users WHERE email LIKE '%@tcgvault.com'"`
> — expect zero rows.
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
## 📄 License
MIT License - see LICENSE file for details