Doc-writer pass for convoy bump-next-js (PR #4 / commit e57ea17).
Single file touched: AGENTS.md (+5 / -1).
- § 1 Project overview: Framework line bumped Next.js 15 -> 16, with
a cross-reference to new Gotcha #9 for the typescript-is-just-for-lint
context.
- § 4 Common gotchas: three new entries that future agents need to
know about but wouldn't infer from the code:
- #9: typescript@^5.9.3 is installed purely so eslint-config-next@16's
bundled typescript-eslint chain can satisfy its hard require('typescript')
at module load. No tsconfig.json, no .ts files, no @ts-check. Decision C.
- #10: ESLint pinned to ^9.39.4 (maintenance), not v10 (latest). v10
surfaced Risk R15 empirically (TypeError: scopeManager.addGlobals)
via @typescript-eslint/scope-manager@8.59.4 predating v10 GA.
Do not bump independently — wait for queued bump-eslint-10
follow-up convoy. Decision D.
- #11: Turbopack is now the default bundler in next dev/build.
Fallback per-command is --webpack. Do not pre-emptively switch.
- § 7 Deployment: reference VERCEL_AUTOMATION_BYPASS_SECRET (env var
name only, no value) for the queued adopt-playwright-smoke convoy
to use against protected preview deploys.
CHANGELOG.md / DEVELOPER_CHANGELOG.md not created — those are deferred
to launch-polish per the convoy's roles section.
README.md staleness (line 16 still says "Next.js 15, React 18,
TypeScript") flagged in the PR description but NOT fixed here per the
docs-pass scope. Pickup: launch-polish.
Co-authored-by: Cursor <cursoragent@cursor.com>
8.6 KiB
AGENTS.md — AI collaboration (tcg-vault)
Guidance for agents and humans working in this repo. Prefer existing patterns over new abstractions.
Branding note: the repo, README, and seed data say "TCG Vault" and
admin@tcgvault.com, but the Layout component renders "Deck Hearth". Pick one before launch — see.convoys/for tracking.
1. Project overview
A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database.
- Framework: Next.js 16 (Pages router) + React 18, JavaScript (not TypeScript — see Gotcha #9)
- Data: Neon Postgres, accessed two different ways —
@neondatabase/serverless(lib/database.js) AND raw@vercel/postgres(pages/api/**). Pick ONE; see Gotcha #1. - Auth: Custom JWT (jsonwebtoken + bcryptjs), token stored in
localStorage, sent asAuthorization: Bearer …. No NextAuth. - UI: Tailwind CSS + custom CSS variables for theming (light/dark via
lib/theme-context.js) - Hosting: Vercel (
vercel.json,.vercel/present)
2. Architecture quick reference
| Area | Path | Notes |
|---|---|---|
| Pages router views | pages/*.js |
Public + auth views; uses components/Layout.js |
| API routes | pages/api/**/*.js |
Express-style handler(req, res). 30+ handlers depend on lib/permission-middleware.js::getUserFromRequest |
| Shared UI | components/*.js |
Layout, CardItem, CameraScanner, modal family |
| Auth + DB libs | lib/*.js |
auth-context, admin-auth, use-auth (three parallel auth surfaces), database, permission-middleware |
| Migration scripts | scripts/*.js |
27+ one-off "add column" / "seed" scripts. No formal migration tool |
| Card-import jobs | pages/api/cards/import-*.js, scripts/import-*.js |
Scryfall / Lorcana / Pokémon TCG APIs |
| Database schema | scripts/setup-neon-db.js |
Bootstrap SQL DDL — the source of truth until a real migration tool lands |
| Schema map | docs/SCHEMA_MAP.md |
Hand-curated; regenerate after schema changes |
Code graph is indexed by user-code-review-graph MCP (122 files, 628 nodes, 5602 edges). Ask: "what calls getUserFromRequest?" before refactoring auth.
3. Key conventions
- Auth (server):
import { getUserFromRequest } from '../../lib/permission-middleware'→ returns{ userId, email, role }ornull. IMPORTANT: the current implementation returns a hardcoded admin user when no Bearer token is present — treat that as a known prod bug, do NOT copy the pattern. - Auth (client):
import { useAuth } from '../lib/use-auth'. Avoidlib/auth-context.jsandlib/admin-auth.jsfor new code — they are legacy parallel implementations. - Auth helper (JWT only):
import { ... } from '../../lib/api/auth-utils'(generateToken,verifyToken,hashPassword,verifyPassword). - Permission gate for collection routes: wrap handlers with
withCollectionPermission('viewer' | 'editor' | 'owner')fromlib/permission-middleware.js. - DB access: Use tagged-template style —
import { sql } from '@vercel/postgres'. Avoid the legacylib/database.jsdb.query(string, params)API; its parameter interpolation usessql.unsafeand is a SQL-injection vector. - Activity logging:
logCollectionActivity(collectionId, userId, action, details)— call it from any handler that mutates a collection. - File names:
kebab-case.jsfor libs/scripts;PascalCase.jsfor React components. - Imports: No path aliases configured; use relative imports.
- Slugs:
lib/slug-utils.js::generateUniqueSlugfor any user-facing identifier (collections, decks). - CSS theme tokens: Components read
var(--bg-primary),var(--text-primary),var(--accent-ember), etc. — defined instyles/. Don't hardcode hex colors.
4. Common gotchas
- #1 — Two SQL clients live in parallel.
@neondatabase/serverless(used bylib/database.js) and@vercel/postgres(used by mostpages/api/**handlers). New code: prefer@vercel/postgrestagged templates. Migration to a single client is tracked in.convoys/. - #2 —
getUserFromRequesthas a dev fallback shipped to prod. When no Bearer token is present it returns user 1 as admin. This is a critical security issue, NOT a feature. Don't rely on it; treat unauthenticated requests as 401. - #3 — JWT_SECRET default is hardcoded across 7 files. If
process.env.JWT_SECRETis unset, tokens are signed with'your-secret-key-change-in-production'. The Vercel project MUST setJWT_SECRET; CI/staging too. - #4 — Default admin credentials are in the seed.
admin@tcgvault.com/admin123fromscripts/setup-neon-db.js. Change the password immediately after running setup. - #5 —
pages/api/setup-database.jsis a public endpoint. Anyone hitting it triggers DB DDL. Either delete or gate behind admin auth before public launch. - #6 — Migrations are bare scripts.
scripts/add-*.jsandscripts/fix-*.jsare run-once jobs with no idempotency tracking. Adoptnode-pg-migrate,kysely, ordrizzle-kitbefore more schema changes. - #7 — Dual
is_publicsemantics. Collections and decks both haveis_publiccolumns; check which controls discovery vs. anonymous read in the relevant route. - #8 — Layout has hardcoded default user.
Layout({ user = { email: 'me@randallstillwell.com', role: 'user' } }). Anything rendering Layout without passinguserwill impersonate the maintainer. Passuserexplicitly from every page. - #9 —
typescriptis a devDep, but the source is still JavaScript-only.package.jsonliststypescript@^5.9.3purely soeslint-config-next@16's bundledtypescript-eslintchain can satisfy its hardrequire('typescript')at module load (thepeerDependenciesMeta.typescript.optional: trueflag ineslint-config-nextonly suppresses npm's install-time warning, not the runtime require). There is notsconfig.json, no.ts/.tsxfiles, and no// @ts-checkdirectives. Do not rename.jsfiles to.tsor add atsconfig.jsonwithout an explicit convoy decision — TypeScript adoption is its own scope. See.convoys/bump-next-js.md§ Decisions C. - #10 — ESLint pinned to v9 (maintenance), not v10 (latest).
devDependencies.eslintis^9.39.4even thoughlatestis10.4.0. We tried v10 andnpm run lintcrashed withTypeError: scopeManager.addGlobals is not a functionbecauseeslint-config-next@16's bundledtypescript-eslint@8.xpredates ESLint v10's redesigned global-ingestion path. Reverted to v9 under Decision D. Do NOT bump ESLint independently — wait for the queuedbump-eslint-10follow-up convoy, which is upstream-blocked untiltypescript-eslintships a v10-tested release thateslint-config-nextbundles. See.convoys/bump-next-js.md§ Decisions D + "Follow-up convoys queued". - #11 — Turbopack is now the default bundler.
next devandnext builduse Turbopack by default in Next.js 16. The fallback per command is--webpack(e.g.next build --webpack). We have no customwebpack:block innext.config.js, no custom loaders/aliases, and no Sass tilde imports, so Turbopack should "just work" — but if a build/runtime regression appears, reproduce on both bundlers before deciding whether to revert or pin a script to webpack. Do not pre-emptively switch to--webpack.
5. Running locally
- Runtime: Node 20 (Vercel default).
- Setup:
npm install, copy.env.localtemplate (POSTGRES_URL + JWT_SECRET + RESEND_API_KEY + BLOB_READ_WRITE_TOKEN), thennpm run setup-dbonce. - Dev server:
npm run dev→ http://localhost:3000.
6. Testing
- Runner: None yet. Adding
vitest+@playwright/testis in.convoys/. Until then: manual smoke perTESTING_GUIDE.md.
7. Deployment
- Vercel auto-deploys
mainand creates Preview deployments for every PR.vercel.jsonand.vercel/are committed. CI in.github/workflows/runs lint + types (no duplicate build — Vercel handles it). - Preview protection bypass for automation. The project has a Protection Bypass for Automation token exposed locally as
VERCEL_AUTOMATION_BYPASS_SECRETin.env.local(not committed). Smoke/visual-diff workflows pass this header (x-vercel-protection-bypass) when hitting password-protected preview URLs. Needed for the queuedadopt-playwright-smokeconvoy; do not log or echo the value.
8. Code graph
A local code-knowledge-graph MCP server (user-code-review-graph) is set up for this repo. Ask "what calls X?" or "show me the flow from /api/auth/login" instead of grepping. See docs/agent-context/README.md.