Convoy: single-sql-client (P1 quality, launch sequence step 8) Addresses: AGENTS.md Gotcha #1, .convoys/ship-readiness.md P1 #8 ## Decisions - D1: Caller inventory = 2 files (1 source + 1 test), not "~3 based on graph". Only pages/api/auth-utils.js imports `db`; test/api/auth-utils.test.js mocks it purely to satisfy the import graph (the 5 tests exercise generateToken/verifyToken, not isAdmin/getUserById). - D2: Migrate both call sites (isAdmin, getUserById) to @vercel/postgres tagged-template SQL. Queries are SELECT-only, single-table, single-numeric-parameter — byte-equivalent translation; same result shape ({rows, rowCount}); no transaction or pool semantics differ. - D3: KEEP @neondatabase/serverless as a dep. 11 scripts/* files still use `neon()` directly (setup-neon-db.js, migrations/, reset-db.js, 8 historical add-*/fix-*/seed-* jobs). They are out of scope per the no-go-zones rule and the convoy spec; purging the dep entirely would be its own convoy (queued as `purge-neondatabase-serverless-fully`, blocked on migration-tool). - D4: sql.unsafe audit — NOT a real injection vector with current callers (userId comes from a verified JWT, is a numeric SERIAL id). Security finding: NO. Pure refactor + foot-gun removal that prevents the FUTURE caller that would have been the incident. - D5: Test mock cleanup — drop the now-unneeded `vi.mock('../../lib/database.js')` call + unused `vi` import. Test count + assertions unchanged (5/5). ## Per-file changes - pages/api/auth-utils.js: swap `import { db } from '../../lib/database.js'` for `import { sql } from '@vercel/postgres'`; rewrite isAdmin's `db.query(SELECT … WHERE id = $1, [userId])` and getUserById's same shape to `sql\`SELECT … WHERE id = ${userId}\``. Same try/catch, same result.rows[0] access, same error returns. - test/api/auth-utils.test.js: drop vi.mock for lib/database.js + the unused `vi` import. 5/5 tests still pass. - lib/database.js: DELETED (47 lines removed; manual-interpolation + sql.unsafe wrapper is gone). - .convoys/single-sql-client.md: NEW (the convoy file documenting all decisions + caller inventory + verification + risks + follow-ups). ## Verification - npm run lint → 128 problems (baseline preserved, no regression) - npm run test:run → 21/21 pass (vitest) - Grep "lib/database" --type js -l → 0 hits anywhere - Grep "@neondatabase/serverless" --type js -l → still matches the 11 scripts/* sites (expected; out of scope per D3) - node --check pages/api/auth-utils.js → exit 0 ## Scope note This convoy collapses the lib/database.js abstraction onto the canonical @vercel/postgres surface for pages/api/**. It does NOT eliminate @neondatabase/serverless from the dependency tree — that would require migrating the scripts/* helpers, which is out of scope here (no-go-zones rule + convoy spec). Queued as a follow-up. ## Live smoke Deferred. The two migrated functions (isAdmin, getUserById) are only reachable via pages/api/admin/index.js which requires an admin Bearer token and a populated users table in prod Neon. Byte-equivalent SQL + identical result shape gives high confidence; rollback is a single-commit revert if a post-merge admin action 500s. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .convoys | ||
| .cursor | ||
| .github | ||
| components | ||
| docs | ||
| lib | ||
| pages | ||
| public | ||
| scripts | ||
| styles | ||
| test | ||
| tests | ||
| .agent-context-manifest.yml | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| eslint.config.mjs | ||
| next.config.js | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.js | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| TESTING_GUIDE.md | ||
| vercel.json | ||
| vitest.config.js | ||
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.mdGotcha #9) - Backend: Next.js API Routes
- Database: Neon PostgreSQL (serverless)
- Authentication: JWT with bcrypt (24-hour expiry;
lib/auth-secret.jsis the single source of truth forJWT_SECRET) - Rate limiting:
@upstash/ratelimiton/api/auth/login+/api/auth/register(5 attempts / 15 min per IP) - Testing: Vitest (unit); Playwright queued
- Styling: Tailwind CSS
- Deployment: Vercel
📦 Installation
-
Clone the repository
git clone <repository-url> cd tcg-vault -
Install dependencies
npm install -
Set up environment variables
cp .env.example .env.localUpdate
.env.localwith 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_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. -
Set up the database
npm run setup-db -
Start development server
npm run dev
🗄️ 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
This app is configured for deployment on Vercel:
- Connect your repository to Vercel
- Set environment variables in Vercel dashboard
- 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_PASSWORDin.env.localbefore runningnpm run setup-db. Useopenssl rand -base64 24(or any other strong source) to generate the value. - CI / Vercel: set
ADMIN_INITIAL_PASSWORDas 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-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