TCG Vault - Trading Card Game Collection Management with OCR Scanning
Find a file
Randall Stillwell 7832e03dec docs: post-convoy cleanup for add-rate-limiting — MILESTONE, last P0 closed
Reflects the merged add-rate-limiting convoy (PR #20, squash commit
708ef45) in repo documentation. **This is the milestone cleanup** —
add-rate-limiting closed P0 #6 (No rate limiting anywhere), the LAST
open P0 ship-blocker. `.convoys/ship-readiness.md`'s § Status summary
flips from "7 of 8 RESOLVED; 1 remains" to **"8 of 8 RESOLVED.
Launch-readiness P0 checklist is empty."** One brief in the convoy:
Brief 1 shipped as planned with no scope expansions and no implementer
deviations from the verbatim spec; all six architect decisions ratified
verbatim at gate 1 (D1 operator-ratified Option A; D2-D6
architect-self-ratified).

.convoys/add-rate-limiting.md:
  - frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
  - new ## As-shipped section. Opens with the milestone language
    pointing back at ship-readiness.md's flipped § Status summary.
    Decisions section captures all 6 ratifications (D1 operator-
    ratified Option A — Critical: WHY the atomic admin UI fix in
    pages/admin/card-import.js was Decision 1's hidden coupling
    requirement, since API gating alone would have broken every
    "Import Cards" click; D2 hybrid named-limiter shape with
    Map<className, Ratelimit> cache; D3 per-class table including
    the two D3 tuning-evidence raises — search 30 -> 60/min because
    ShareModal.handleSearch has no debounce so a 17-char email = 16
    requests in <5s, and generate kept at 5/hour because DiceBear is
    free not paid AI; D4 two-extractor shape with defensive THROW
    on null/empty userId; D5 uniform 429 message; D6 no new vitest
    or playwright specs deferred to fill-vitest-handler-coverage).
    As-shipped surface broken into 4 layers (1 lib refactor + 6 route
    gates + 1 atomic admin UI fix + 1 rule extension) mirroring the
    cors-tighten cleanup's pattern-split shape. Empirical CI metrics
    from post-merge run 26382185019 (Playwright smoke 59s 3/3 in
    3.8s, forbidden-cors-headers pass, vitest 21/21, lint 128 baseline,
    Screenshot diff continue-on-error swallow per Decision 4).
    Cross-validation finding: smoke test 2 still passes against the
    post-rate-limit preview — that's three convoys in a row (PR #15
    Layout default-user, PR #19 CORS-tighten, PR #20 rate-limiting)
    where the same 3-test smoke spec defended the auth surface
    through sweeping changes. Operator-action-required: none. What
    did NOT change audit trail.

.convoys/ship-readiness.md:
  - § Status summary at the top flipped from 7/8 to 8/8 RESOLVED.
    Header text updated to "Launch-readiness P0 checklist is empty."
    P0 #6 row in the table flips from PARTIAL to RESOLVED with the
    two-convoy lineage (fix-auth-bypass Brief 4 + add-rate-limiting).
    Trailing paragraph rewritten as a milestone note: security gate
    closed; remaining launch work is P1 quality bar + P2/P3 polish.
  - P0 #6 entry flipped from PARTIAL to RESOLVED with the full
    add-rate-limiting as-shipped block (8 sub-bullets covering the
    lib refactor shape, the per-class table, the defensive THROW,
    the three import routes' auth-gating, the atomic admin UI fix
    and WHY, the rule extension, the 6 decisions, and the diff
    breakdown). Brief 4's 2026-05-23 partial is preserved as the
    prior as-shipped layer to maintain the audit trail.
  - Launch sequence step 4 marked RESOLVED 2026-05-24 with the
    squash commit + smoke metrics inline.
  - Queued convoys: removed the add-rate-limiting entry (it shipped).
    Added a new delete-dead-lorcana-import entry (P3 polish; the
    Lorcana import route was gated defensively in PR #20 despite
    zero current frontend callers — pages/admin/card-import.js's
    <select> only offers mtg + pokemon — so if Lorcana stays
    permanently out of the admin UI, this is the cleanup PR).
    Added three "flagged but kept out of scope" follow-ups per the
    convoy's § What did NOT change: harden-multipart-parser (P2;
    5MB body still consumed before the 429 path on avatar.js),
    god-function-split / refactor-cards-search-sql (P2; 240-line
    7-branch SQL in cards/search.js), and withAdmin(handler) wrapper
    extraction (P3 DX; the three import routes are call sites #3-5
    in the codebase but uniform inline shape was preserved for
    convoy atomicity). Updated tighten-visual-diff-path-filter to
    note PR #20 also tripped the same false-positive.

AGENTS.md:
  - Gotcha #12 extended end-to-end. Was the single-class auth-only
    lib + the env-var contract; is now the 5-class reality with a
    full per-class table (helper / limit-window / key / routes),
    the defensive THROW pattern in extractUserIdentifier, the
    gate-ordering rule for per-user limiters, and the
    auth → admin-role → rate-limit ordering for the three import
    routes. Prominent milestone line opens the new content:
    "add-rate-limiting convoy (squash 708ef45, PR #20, 2026-05-24)
    closed P0 #6 — all 8 P0s now RESOLVED." Original env-var
    contract paragraph (KV_REST_API_URL / KV_REST_API_TOKEN,
    fail-closed-in-prod / warn-and-noop-in-dev) is preserved
    verbatim above the new content.
  - § 6 Testing: intentionally untouched (no test surface changed;
    vitest 21/21 and smoke 3/3 still apply).
  - § 7 Deployment: intentionally untouched (no deployment-shape
    changed; same KV_REST_API_* env vars from Brief 4).

.cursor/rules/api-routes.mdc:
  - The implementer extended § Rate limiting in PR #20 with the
    per-class table + verbatim call shape + gate-ordering rules +
    identifier-extraction + uniform 429 + fail-closed env-var
    contract + fail-open Upstash-outage behavior. Doc-writer pass
    verified completeness; added a one-sentence convoy-attribution
    line at the top of § Rate limiting citing the two-convoy
    lineage (fix-auth-bypass Brief 4 for the auth class +
    add-rate-limiting for the other four classes and 7 newly-gated
    routes), mirroring the post-cors-tighten § CORS attribution
    shape. No other touch-ups needed.

No changes to: package.json, package-lock.json, lib/rate-limit.js,
pages/**, components/**, scripts/**, test/**, tests/**,
.github/workflows/**, README.md, TESTING_GUIDE.md, playwright.config.js,
eslint.config.mjs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:09:07 -05:00
.convoys docs: post-convoy cleanup for add-rate-limiting — MILESTONE, last P0 closed 2026-05-24 23:09:07 -05:00
.cursor docs: post-convoy cleanup for add-rate-limiting — MILESTONE, last P0 closed 2026-05-24 23:09:07 -05:00
.github fix(security): drop wildcard CORS + redundant OPTIONS from 24 API routes (P0 #5) 2026-05-24 20:41:38 -05:00
components fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) 2026-05-24 14:31:37 -05:00
docs bootstrap: agent pipeline v0.5.0 + ship-readiness review 2026-05-23 02:31:26 -05:00
lib feat(security): rate-limit search/upload/import + gate import routes (P0 #6 - closes last P0) 2026-05-24 22:59:59 -05:00
pages feat(security): rate-limit search/upload/import + gate import routes (P0 #6 - closes last P0) 2026-05-24 22:59:59 -05:00
public Major redesign: Enhanced card display with particle effects, improved filters, and search functionality 2025-07-23 21:26:54 -05:00
scripts fix(seed): convert scripts/setup-neon-db.js from CJS to ESM (Node 22.x compat) 2026-05-23 17:02:44 -05:00
styles 🚀 Implement Mobile-First Navigation System 2025-08-01 18:18:21 -05:00
test fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) 2026-05-24 14:31:37 -05:00
tests feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2) 2026-05-24 19:25:18 -05:00
.agent-context-manifest.yml bootstrap: agent pipeline v0.5.0 + ship-readiness review 2026-05-23 02:31:26 -05:00
.gitignore feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2) 2026-05-24 19:25:18 -05:00
.npmrc Fix build compatibility issues 2025-07-22 10:32:15 -05:00
AGENTS.md docs: post-convoy cleanup for add-rate-limiting — MILESTONE, last P0 closed 2026-05-24 23:09:07 -05:00
eslint.config.mjs bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep 2026-05-23 02:31:26 -05:00
next.config.js bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep 2026-05-23 02:31:26 -05:00
package-lock.json feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2) 2026-05-24 19:25:18 -05:00
package.json feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2) 2026-05-24 19:25:18 -05:00
playwright.config.js feat(test): adopt @playwright/test + ship playwright.config.js + visual scaffold (P1 #10 step 2) 2026-05-24 19:25:18 -05:00
postcss.config.js Major redesign: Enhanced card display with particle effects, improved filters, and search functionality 2025-07-23 21:26:54 -05:00
README.md feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README 2026-05-23 17:02:44 -05:00
tailwind.config.js Ultra-Smooth Hover System with Expanding Actions 2025-07-26 09:00:26 -05:00
TESTING_GUIDE.md 🎨 Restructured Layout System 2025-07-25 11:28:04 -05:00
vercel.json Simplify Vercel config to resolve API routing issues 2025-07-23 09:22:14 -05:00
vitest.config.js fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) 2026-05-24 14:31:37 -05:00

TCG Vault

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

    git clone <repository-url>
    cd tcg-vault
    
  2. Install dependencies

    npm install
    
  3. Set up environment variables

    cp .env.example .env.local
    

    Update .env.local with your Neon database URL and a real JWT secret:

    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 requiredlib/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

    npm run setup-db
    
  5. Start development server

    npm run dev
    

🗄️ 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 creates a single admin user the first time it runs. 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@tcgvault.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.

🤝 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