- README: Dokploy/CT102 Postgres stack, lib/sql.js, six rate-limit classes over REDIS_URL, Playwright smoke+visual, MinIO/S3 vars, inline .env.local contract (no .env.example exists), deckhearth.git clone URL, project-structure refresh. - AGENTS.md: infra banner for the Vercel/Neon -> homelab move, repo rename to stwl-labs/deckhearth, Data/Auth/Hosting overview bullets, DB-access convention re-pointed at lib/sql.js, Gotcha #12 rewritten for REDIS_URL + scan limiter (+ row in limiter table), test counts refreshed (231/231 across 43 files), §5 env contract, §7 marked legacy-pending-decommission with DOKPLOY_DEPLOY runbook pointer.
9.4 KiB
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 nowstwl-labs/deckhearth. Local checkout folders namedtcg-vaultare 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.mdGotcha #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 inlib/sql.js— seedocs/HOMELAB_DATABASE.md - Authentication: JWT with bcrypt (24-hour expiry;
lib/auth-secret.jsis the single source of truth forJWT_SECRET) - Rate limiting:
lib/rate-limit.js— six named limiters (auth,search,upload,generate,import,scan) backed byioredis+rate-limiter-flexibleagainst homelab Redis (CT 102). Fails closed in production ifREDIS_URLis unset; warn-and-no-op in dev - Testing: Vitest (unit) + Playwright (
smokeandvisualprojects) - 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.cloudvia Traefik — seedocs/DOKPLOY_DEPLOY.md
📦 Installation
-
Clone the repository
git clone https://github.com/stwl-labs/deckhearth.git cd deckhearth -
Install dependencies
npm install -
Create
.env.local(there is no committed template — use the shape below)# Required — homelab Postgres (CT 102) or any Postgres 17 instance POSTGRES_URL="postgresql://deckhearth:<password>@192.168.68.102:5432/deckhearth" # Migrations (`npm run migrate`); on the homelab, same value as POSTGRES_URL POSTGRES_URL_DIRECT="postgresql://deckhearth:<password>@192.168.68.102:5432/deckhearth" JWT_SECRET="<generate with: openssl rand -hex 32>" # Required for `npm run setup-db` — used once to hash the initial admin password. ADMIN_INITIAL_PASSWORD="<generate with: openssl rand -base64 24>" # 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_SECRETis required —lib/auth-secret.jsthrows at import time if it's unset.ADMIN_INITIAL_PASSWORDis required fornpm run setup-db— the script exits with code 1 if it's unset.Migrating data off the old Neon instance?
NEON_DATABASE_URLis read once bynpm run migrate-neon-to-homelab. Seedocs/HOMELAB_DATABASE.md. -
Set up the database
npm run setup-dbThis applies every pending migration under
migrations/(vianode-pg-migrate) and then seeds the initial admin user. To run just the migration step without seeding, usenpm run migrate up. -
Start development server
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).
# 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 authenticationcards- Card information and metadatauser_cards- User's card collectionscollections- Named card collectionscollection_cards- Cards in collectionsdecks- Deck definitionsdeck_cards- Cards in decks
🔧 API Endpoints
Authentication
POST /api/auth/register- User registrationPOST /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 infix-auth-bypassBrief 3 (commitfc0dd73) 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.
Transition note: the Vercel + Neon era is being decommissioned (
migrate-neon-to-homelabconvoy, phases 6–8).vercel.jsonand.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_PASSWORDin.env.localbefore runningnpm run setup-db. Useopenssl rand -base64 24(or any other strong source) to generate the value. - CI: set
ADMIN_INITIAL_PASSWORDas 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-dbis idempotent (ON CONFLICT (email) DO NOTHING) — re-running it withADMIN_INITIAL_PASSWORDset 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 queuedrotate-default-adminfollow-up convoy.
Operators of envs that pre-date the
pick-a-nameconvoy (2026-05-24): the admin row was renamed fromadmin@tcgvault.comtoadmin@deckhearth.com. Runnode scripts/migrations/2026-05-24-rename-admin-email.jsonce after deploy to UPDATE any existing@tcgvault.comuser rows (the admin row, plus alice/bob ifnpm run create-test-userswas ever run). Re-running the migration after the first run is idempotent and prints "Nothing to migrate." Verify post-migration withpsql $POSTGRES_URL -c "SELECT email FROM users WHERE email LIKE '%@tcgvault.com'"— expect zero rows.
🤝 Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
📄 License
MIT License - see LICENSE file for details