- 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.
216 lines
No EOL
9.4 KiB
Markdown
216 lines
No EOL
9.4 KiB
Markdown
# 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:<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_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/<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
|
||
|
||
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 |