deckhearth/.convoys/ship-readiness.md
Randall Stillwell 1944b1ed48 bootstrap: agent pipeline v0.5.0 + ship-readiness review
Installs the three-layer agent-pipeline scaffold (https://github.com/varutasu/agent-pipeline @ v0.5.0):

L1 — Context (curated brain)
- AGENTS.md: orientation, conventions, 8 explicit gotchas
- .cursor/rules/: no-go-zones, api-routes, auth-and-permissions,
  db-and-schema, ui-and-theming, schema-map
- .cursor/skills/: add-api-route, add-page recipes
- docs/agent-context/README.md: layer explainer
- docs/SCHEMA_MAP.md: hand-curated Neon Postgres reference
  (replaces Prisma schema map since stack is raw SQL)

L2 — Subagent roles (copied verbatim from upstream templates)
- 9 .cursor/agents/role-*.md files: Conductor, IA-Architect,
  UX-Reviewer, Architect, Implementer, Reviewer,
  Design-System-Auditor, A11y-Auditor, Doc-Writer

L3 — Pipeline scaffolding (Vercel variant)
- CI: lint + schema-map-drift only (no duplicate build —
  Vercel handles it). Test job commented out until vitest lands.
- preview-smoke + visual-diff via wait-for-vercel-preview
- pr-health-rollup sticky comment aggregator
- agent-context-drift weekly cron
- PULL_REQUEST_TEMPLATE, CODEOWNERS (auth/admin paths tagged)
- .convoys/ folder + seed ship-readiness.md review
- lib/flags/index.js (JS — converted from TS template)
- scripts/wt.sh (Cursor 3.2 deprecation stub),
  scripts/log-convoy-event.sh
- tests/smoke/app.smoke.spec.ts (Playwright skeleton)

Manifest
- .agent-context-manifest.yml: tracks 31 artifacts by sha256
  for future sync-agent-context drift detection

Review
- .convoys/ship-readiness.md: 16 findings (7 P0 ship-blockers,
  5 P1 quality-bar, 4 P2 refactor, P3 UX/IA/a11y/docs) with
  proposed 13-convoy launch sequence.

No production code changed in this commit. All findings in
the ship-readiness review will be addressed in follow-up convoys
starting with fix-auth-bypass.

Structural brain: user-code-review-graph MCP has indexed the
codebase (122 files, 628 nodes, 5602 edges, 11 communities,
84 flows). Per-developer; not committed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 02:31:26 -05:00

18 KiB
Raw Blame History

name classification success_metric skip status created
ship-readiness epic tcg-vault is safe to expose to anonymous internet traffic with a documented launch checklist green
open 2026-05-22

Ship-readiness convoy

Umbrella convoy capturing the full agent-pipeline review of tcg-vault as of 2026-05-22. Findings are grouped by L2 role lens (Reviewer / Architect / Design-system / A11y / IA / Doc-writer) and severity. Each item points to the convoy that will execute the fix.

Code graph: 122 files, 628 nodes, 5602 edges, 11 communities. Indexed by user-code-review-graph MCP.

P0 — ship-blockers (security)

These MUST land before any anonymous traffic touches the production URL.

1. getUserFromRequest returns a hardcoded admin when no Bearer token is present

  • File: lib/permission-middleware.js lines 13-17.
  • Impact: Every API route that calls getUserFromRequest (30+ handlers — see user-code-review-graph cross-community edges from api-handlerlib-admin) accepts unauthenticated requests as admin user 1.
  • Repro: curl https://<host>/api/collections with no Authorization header returns admin's collections.
  • Fix: Delete lines 13-17. Return null when no Bearer token. Update every caller to handle null properly (most already do; the broken fallback was masking the right path).
  • Owns: role-architect + role-implementer (one PR; small surface area in the helper, callers already check !user).

2. JWT_SECRET hardcoded fallback in 7 files

  • Files:
    • pages/api/auth-utils.js ('your-secret-key')
    • pages/api/auth/login.js, pages/api/auth/register.js, pages/api/auth/verify.js
    • pages/api/favorites.js, pages/api/users/search.js
    • lib/permission-middleware.js
  • Impact: If JWT_SECRET env var is unset (e.g. preview/staging misconfig), tokens are signed with 'your-secret-key-change-in-production' — an attacker can sign their own admin token in 5 seconds.
  • Fix: Centralize JWT_SECRET access in one helper that throws at module load if process.env.JWT_SECRET is unset. Every other file imports from there.
  • Bonus: Token expiry is inconsistent (/api/auth/login.js uses 24h, pages/api/auth-utils.js uses 7d). Pick one.
  • Owns: role-architect + role-implementer.

3. Default admin credentials in seed + README

  • Files:
    • scripts/setup-neon-db.js lines 130-138 — creates admin@tcgvault.com / admin123
    • README.md documents the credentials
    • pages/api/setup-database.js — duplicates the setup AND is an UNAUTHENTICATED public POST endpoint with Access-Control-Allow-Origin: *
  • Impact: Anyone who hits /api/setup-database can re-trigger DDL. The admin123 password is one Google away from public knowledge.
  • Fix:
    1. Delete pages/api/setup-database.js. Schema setup is a one-time job; it should not be a route.
    2. Change setup-neon-db.js to require a ADMIN_INITIAL_PASSWORD env var (no default).
    3. Strip the admin password from README — replace with "run npm run setup-db and follow the prompt".
  • Owns: role-implementer.

4. Dev-only test endpoints shipped to production

  • Files: pages/api/simple.js, pages/api/test-auth.js, pages/api/test-db.js, pages/api/setup-database.js.
  • Impact: Unknown — depends on what they expose. /api/test-db likely returns the DB connection string; /api/test-auth may leak token-handling details.
  • Fix: Delete all four. Add a CI grep that fails the build if any file matching pages/api/(test-|simple|setup-)*.js exists.
  • Owns: role-implementer.

5. CORS Access-Control-Allow-Origin: * on auth endpoints

  • Files: at minimum pages/api/auth/login.js, pages/api/auth/register.js, pages/api/setup-database.js (verify others).
  • Impact: Any origin can submit credentials. Combined with the no-rate-limit problem below, credential stuffing is wide open.
  • Fix: Set Access-Control-Allow-Origin to the literal frontend origin (https://tcgvault.com / preview domain), or remove the header entirely if the API and the frontend are same-origin (they are, on Vercel).
  • Owns: role-implementer.

6. No rate limiting anywhere

  • Impact: Login endpoint accepts unlimited attempts; card-search endpoint can be hammered; image upload endpoints can be exhausted. The pages/api/cards/import-*.js endpoints externally hit Scryfall/Pokémon APIs with no caller throttling.
  • Fix: Adopt @upstash/ratelimit (free tier covers a small launch) or Vercel's built-in middleware-based rate limiting. Apply to: /api/auth/login, /api/auth/register, /api/users/search, /api/cards/search, all /api/cards/import-*, and /api/user/avatar* (upload).
  • Owns: role-architect (pattern) → role-implementer (per-route).

7. Layout default-prop leaks maintainer email

  • File: components/Layout.js line 562: function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, ... }).
  • Impact: Any page that renders Layout without passing a user prop displays your real email and impersonates you as the logged-in user.
  • Fix: Default user = null and render a logged-out state branch. Verify every page passes user explicitly (the graph shows ~13 pages call Layout; audit each).
  • Owns: role-implementer.

P1 — pre-launch quality bar

8. Two SQL clients in parallel (@neondatabase/serverless + @vercel/postgres)

  • Impact: Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus lib/database.js's manual interpolation + sql.unsafe(query) is a SQL-injection vector if any caller passes user input through.
  • Fix: Pick @vercel/postgres (tagged-template, no injection vector). Migrate every call site of lib/database.js::db.query. Delete lib/database.js.
  • Reviewer/Architect call: small enough to fit in one convoy; touches ~3 files based on graph.

9. Three parallel client-side auth implementations

  • Files: lib/auth-context.js (AuthProvider / useAuth), lib/admin-auth.js (AdminProvider / useAdmin / useIsAdmin), lib/use-auth.js (useAuth).
  • Impact: Pages randomly import from one of three places. State is duplicated. Logout in one provider doesn't necessarily clear the others. Token-verify roundtrips happen 3× on initial page load if all three providers mount.
  • Fix: Collapse to lib/use-auth.js as the canonical hook. Migrate every importer. Delete auth-context.js and admin-auth.js. Roll up useIsAdmin semantics into useAuth().user?.role === 'admin'.
  • Owns: role-architect (decision) → role-implementer (per-page migration; ~30 importers).

10. No tests

  • Impact: The first agent-driven refactor of getUserFromRequest (P0 #1) is high-blast-radius with no safety net.
  • Fix sequence:
    1. Install vitest. Add npm run test:run script.
    2. Install @playwright/test. Wire up tests/smoke/app.smoke.spec.ts (already drafted; needs playwright.config.ts).
    3. Re-enable the test: job in .github/workflows/ci.yml (commented out at install time).
    4. Add unit tests for lib/permission-middleware.js, lib/slug-utils.js, pages/api/auth-utils.js.
    5. Wire preview-smoke.yml to run against the Vercel preview URL.
  • Owns: role-architect (test strategy) → role-implementer (initial suite).

11. No migration tool — scripts/add-*.js graveyard

  • Files: 27 scripts in scripts/ of the form add-foo-column.js, fix-bar-constraint.js, seed-baz.js. No idempotency tracking, no schema_migrations table, no rollback.
  • Impact: Onboarding a new env requires re-running every script in the right order. No way to know what's been run on a given Neon branch. Every new column is at risk of being missed in prod.
  • Fix: Adopt node-pg-migrate (lightweight, matches the existing pattern best) OR migrate to drizzle-kit if the team wants schema-as-code. Backfill a single "initial" migration matching current prod schema. From there, every new column ships as a migration file.
  • Owns: role-architect (tool selection) → role-implementer (backfill + first new migration).

12. Branding mismatch — "TCG Vault" vs. "Deck Hearth"

  • Files: README, package.json, seed data say "TCG Vault" / admin@tcgvault.com. components/Layout.js lines 596 + 689 render "Deck Hearth" + "DH" logo. The .env.local template, vercel.json, and Vercel project name should also be audited.
  • Impact: Confusing for users. Confusing for marketing. Confusing for analytics. Pick one.
  • Fix: Brand workshop → final name → global replace → update README, package.json "name", every UI string, Vercel project name, email sender, support pages. Schedule a redirect from the old domain.
  • Owns: role-ia-architect (which name? — needs human decision) → role-implementer.

P2 — refactor priorities

13. God components (10 files over 500 lines)

File Lines Notes
pages/cards.js 1499 AuthenticatedCards (886) + Card3D (502) live in one file. Split into pages/cards/index.js + components/Card3D.js.
pages/collection/[identifier].js 1044 CollectionView is one mega-component. Extract: header, card-grid, share-modal-wrapper, edit-form.
pages/collections.js 989 Similar structure to collection/[identifier]. Possibly share extracted pieces.
pages/card/[id].js 913 CardDetail — split into header, owned-badge, add-to-collection-flow.
pages/deck-builder.js 823 DeckBuilder — extract card-search, deck-list, mana-curve panels.
components/CameraScanner.js 817 Camera + AI-OCR + detection-loop — extract the detection loop into a hook.
pages/admin/card-editor.js 778 Form heavy. Use a useFormState pattern + separate the search-results subview.
pages/scanner.js 776 Mirror of CameraScanner concerns plus queue management.
pages/settings.js 669 One screen per settings section is the usual fix.
pages/profile.js 625 Avatar generation logic alone is ~150 lines — extract useGeneratedAvatar hook.

Each is one convoy of its own. Use the architect role's slice_dependencies: to fan out implementers safely.

14. Schema-design smells (documented in docs/SCHEMA_MAP.md)

  • users has two avatar columns (profile_image_url + avatar_url). Reconcile.
  • collections has two visibility flags (is_public BOOLEAN + visibility VARCHAR). Reconcile.
  • cards.quantity + cards.favorited are unused (they belong on user_cards / user_favorites). Drop.
  • user_settings table duplicates several users columns. Reconcile.
  • All enum-shaped VARCHARs (role, condition, theme, game, visibility) should be CHECK-constrained or proper Postgres ENUMs.
  • collections.tags is TEXT (comma-separated). Migrate to JSONB or a join table.

15. Component coupling warning from graph

user-code-review-graph flagged:

  • High coupling (44 edges) between components-handle and pages-handle (largely Layout, CardItem, ManaCost — expected for a shared UI surface).
  • High coupling (34 edges) between lib-admin and api-handler — almost all via getUserFromRequest. After P0 #1 is fixed, this number stays high because the auth check is genuinely shared — that's fine.

16. Lots of inline SVG and emoji

The getIcon registry in Layout.js and MobileNavigation.js redefines the same SVG paths. Extract to components/icons/ with named exports. Then audit the codebase for inline SVG that should be a named import. Bonus: lazy-load the larger icon families.

P3 — UX, IA, design-system

Role-ia-architect findings

  • URL structure — solid. /cards, /collections, /collection/[slug], /deck-builder, /community/collections. Coherent. One quirk: /card/[id] (singular) for detail vs. /cards (plural) for index — typical Next.js shape but worth a redirect rule so /cards/[id] also resolves.
  • Logged-out homepage — current pages/index.js is 316 lines; needs an editorial pass. What's the value prop in one sentence? Right now it's mostly "we have cards".
  • Onboarding — signup → profile setup → first collection → scan-or-import card. Currently each step is a separate page. Consider a multi-step wizard at /onboarding to keep the new user in flow.
  • Discoverability/community/decks and /community/forums are in the nav but flagged as placeholders. Either ship the MVP for each before launch (forums likely too big) or hide the nav items until they exist.

Role-ux-reviewer findings

  • Loading states — most data fetches set loading: true then re-render; very few show skeletons. Card grids should use shimmer placeholders; modals should disable submit while in flight.
  • Error states — error messages bubble to console.error and toast nothing. Add a global toast system (e.g. sonner) and wire every catch block.
  • Empty states/my-cards and /collections when empty drop to "no cards yet". Replace with first-time CTA: "Scan your first card" or "Browse popular sets".
  • Mobile drawerMobileNavigation is solid (recent commit 442e906). One thing: the bottom-bar's active state contrast looks low in light mode; verify against AA.
  • Camera scanner UX — 817 lines of detection loop. Add a one-line "scanning…" status under the viewfinder and a single "captured N cards" badge. The current toolbar is busy.

Role-design-system-auditor findings

  • Two visual languages mixing — Tailwind classes AND CSS variables on the same elements. This is documented in .cursor/rules/ui-and-theming.mdc; the cleanup is to define which property goes where and enforce.
  • Hardcoded hex colors — grep for bg-\[# and style={{ backgroundColor: '#. There are still a handful; convert to theme tokens.
  • Logo + brand — see P1 #12. Then once the name is settled, the "DH" logo + AnimatedFireLogo need to be unified into one brand mark.
  • Modal patternsCollectionSelectionModal, ShareModal, UploadImageModal each have their own backdrop + focus-trap implementation. Extract <Modal> primitive. Use headlessui or radix-ui's Dialog to get focus management for free.
  • Card grid spacing + densitypages/cards.js (the 1499-line monster) does responsive grid math inline. Extract a <CardGrid> component that handles density (compact / comfortable / spacious) + sort + filter chrome.

Role-a11y-auditor findings

  • Focus traps in modals — none of the modals trap focus. Tab through ShareModal and you leave to the background. Critical for keyboard users + screen readers.
  • ESC to close modals — inconsistent. Some have it, some don't.
  • Skip-to-content — no <a href="#main" class="sr-only focus:not-sr-only">. Add to _app.js.
  • Image alts — card images use alt={card.name} (good); avatar images sometimes have empty alts. Audit.
  • Color contrast — verify the muted text colors (var(--text-secondary)) hit AA on both themes. The mobile bottom-bar inactive state is a likely fail.
  • Form errors — login/signup form errors are visually red but not connected to inputs via aria-describedby. Screen readers don't know which field failed.
  • Keyboard ops on non-button elements — most clickable <div>s already have onKeyDown but a few don't (audit with rg "onClick" components pages | rg -v "<button").

Role-doc-writer findings

  • README — needs a public-facing rewrite. Currently mixes user docs + dev setup + admin credentials. Split into README.md (project landing) + docs/DEVELOPMENT.md (dev setup) + delete the admin credentials section entirely.
  • docs/SCHEMA_MAP.md — installed at bootstrap (this convoy). Keep it fresh on every schema change.
  • CHANGELOG — none yet. Adopt Keep-a-Changelog format. Backfill [0.1.0] — initial private alpha covering everything to date.
  • TESTING_GUIDE.md — currently the only test doc; rename to docs/MANUAL_QA.md once vitest + playwright land.
  • docs/API_REFERENCE.md — would help. Could be auto-generated by walking pages/api/**/*.js and extracting JSDoc; or hand-curated to start.
  • Privacy policy / Terms of service — required before public launch. Use a template (Termly / Iubenda) and customize.

Proposed launch sequence

Each phase is one Conductor-created convoy. Don't run more than two in parallel until tests exist.

  1. fix-auth-bypass (P0 #1, #2, #4, #5, #6 partial). One PR. Highest risk; needs human review.
  2. drop-public-setup (P0 #3, #4). One PR. Trivial; do as a hotfix.
  3. fix-layout-default-user (P0 #7). One PR. Trivial.
  4. add-rate-limiting (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes.
  5. pick-a-name (P1 #12). Human decision first, then one or two PRs.
  6. adopt-vitest (P1 #10 step 1). One PR. Enables testing every future change.
  7. migration-tool (P1 #11). One PR. Backfill + first new migration.
  8. single-sql-client (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written.
  9. single-auth-provider (P1 #9). 3-5 PRs via multitask.
  10. adopt-playwright-smoke (P1 #10 step 2). One PR.
  11. schema-cleanup (P2 #14). Multi-PR convoy via multitask.
  12. god-component-split (P2 #13). One convoy per file; fan out via multitask once architect's slice_dependencies are written.
  13. launch-polish (P3). UX/IA/a11y/docs convoy.

Total: ~13 convoys to get from current state to public-launch-ready. Estimate 4-8 weeks at one human-in-the-loop reviewer per convoy. Multitask + Cursor 3.2 worktrees compress steps 8-12 substantially.

Self-analytics

After each convoy, scripts/log-convoy-event.sh emits a record to .convoys/.metrics.jsonl (gitignored). After 3-5 convoys, run the upstream agent-pipeline/analytics/ aggregator to see where token spend goes — that data feeds whether to add or remove rules.

How to start

Per .cursor/agents/role-conductor.md, start the next convoy with:

"Run role-conductor: start a new convoy fix-auth-bypass to address P0 #1, #2, #4, #5, #6 partial in .convoys/ship-readiness.md. Success = getUserFromRequest returns null for missing tokens; no API route accepts unauthenticated requests; CI green."

The Conductor will set classification, skip flags, and hand off to subsequent roles.