# Deck Hearth A modern trading card game collection manager built with Next.js and PostgreSQL. > Repo history note: this project was formerly **TCG Vault** (`tcg-vault`); > the GitHub repo is now [`stwl-labs/deckhearth`](https://github.com/stwl-labs/deckhearth). > Local checkout folders named `tcg-vault` are fine. ## πŸš€ 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**: PostgreSQL 17 + pgvector on the axiom homelab (CT 102, `192.168.68.102:5432`), accessed via the tagged-template helper in `lib/sql.js` β€” see `docs/HOMELAB_DATABASE.md` - **Authentication**: JWT with bcrypt (24-hour expiry; `lib/auth-secret.js` is the single source of truth for `JWT_SECRET`) - **Rate limiting**: `lib/rate-limit.js` β€” six named limiters (`auth`, `search`, `upload`, `generate`, `import`, `scan`) backed by `ioredis` + `rate-limiter-flexible` against homelab Redis (CT 102). Fails closed in production if `REDIS_URL` is unset; warn-and-no-op in dev - **Testing**: Vitest (unit) + Playwright (`smoke` and `visual` projects) - **Object storage**: MinIO on CT 102 (scan captures; S3-compatible via `@aws-sdk/client-s3`) - **Styling**: Tailwind CSS + Liquid Glass design tokens (`docs/DESIGN_TOKENS.md`) - **Deployment**: Dokploy on CT 112, public URL `https://deckhearth.stillwell.cloud` via Traefik β€” see `docs/DOKPLOY_DEPLOY.md` ## πŸ“¦ Installation 1. **Clone the repository** ```bash git clone https://github.com/stwl-labs/deckhearth.git cd deckhearth ``` 2. **Install dependencies** ```bash npm install ``` 3. **Create `.env.local`** (there is no committed template β€” use the shape below) ```env # Required β€” homelab Postgres (CT 102) or any Postgres 17 instance POSTGRES_URL="postgresql://deckhearth:@192.168.68.102:5432/deckhearth" # Migrations (`npm run migrate`); on the homelab, same value as POSTGRES_URL POSTGRES_URL_DIRECT="postgresql://deckhearth:@192.168.68.102:5432/deckhearth" JWT_SECRET="" # Required for `npm run setup-db` β€” used once to hash the initial admin password. ADMIN_INITIAL_PASSWORD="" # Rate limiting (homelab Redis, CT 102 β€” DB index 5 in prod). Optional in dev β€” # without it, `lib/rate-limit.js` warn-and-no-ops locally; production fails closed. REDIS_URL="redis://:password@192.168.68.102:6379/5" # Scan-capture uploads (MinIO on CT 102, S3-compatible β€” see lib/object-storage.js) S3_ENDPOINT="http://192.168.68.102:9000" S3_BUCKET="deckhearth" S3_ACCESS_KEY_ID="..." S3_SECRET_ACCESS_KEY="..." S3_PUBLIC_BASE_URL="https://cdn.stillwell.cloud/deckhearth" S3_REGION="us-east-1" ``` `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. > Migrating data off the old Neon instance? `NEON_DATABASE_URL` is read once by > `npm run migrate-neon-to-homelab`. See `docs/HOMELAB_DATABASE.md`. 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/_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 Deck Hearth deploys to **Dokploy on CT 112** in the axiom homelab, fronted by Traefik on CT 100 at `https://deckhearth.stillwell.cloud`. The full runbook β€” Dokploy app settings, environment variables, Traefik route, and the n8n catalog-sync cron that replaced Vercel Cron β€” lives in [`docs/DOKPLOY_DEPLOY.md`](docs/DOKPLOY_DEPLOY.md). > **Transition note:** the Vercel + Neon era is being decommissioned > (`migrate-neon-to-homelab` convoy, phases 6–8). `vercel.json` and `.vercel/` > remain in the tree until that decommission lands; CI already gates against > the homelab deployment. ## πŸ“ Project Structure ``` deckhearth/ (formerly 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 β”œβ”€β”€ components/ # React components (+ scanner/ subfamily) β”œβ”€β”€ lib/ # Utility libraries β”‚ └── sql.js # Canonical Postgres client (tagged templates) β”œβ”€β”€ migrations/ # node-pg-migrate migrations (source of truth for DDL) β”œβ”€β”€ scripts/ # Setup, import, and historical one-off jobs β”œβ”€β”€ docs/ # Deploy / DB / design-token runbooks β”œβ”€β”€ test/ + tests/ # Vitest unit specs; Playwright smoke + visual specs └── .env.local # Environment variables (not committed) ``` ## πŸ” 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:** set `ADMIN_INITIAL_PASSWORD` as a repo 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