deckhearth/README.md
Randall Stillwell 68bf75f18c feat(infra): adopt node-pg-migrate + backfill initial schema migration
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-26 22:55:09 -05:00

198 lines
No EOL
7.4 KiB
Markdown

# 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
- **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)
- **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
```
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"
JWT_SECRET="<generate with: openssl rand -hex 32>"
# 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>"
# 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>"
```
`JWT_SECRET` is **required**`lib/auth-secret.js` throws at import time if it's unset.
`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
```
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
```
## 🧱 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
> **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
```
## 🔐 First-time admin setup
`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
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.
- **Admin email:** the seed creates `admin@deckhearth.com`. Change the password
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.
> **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