Skip to content

Project State โ€‹

Updated: 2026-10-04 (Phase 3.3 Card API complete โ€” the HTTP + front-end layers landed on top of the search data/service of #122. Routes (#124): four public /api/v1/cards endpoints (search / :id / :id/prints / autocomplete), card-mapper (Prisma value โ†’ DTO: narrow Json/array types, ISO dates, drop the sets aggregate), a reusable toPaginated builder next to makePaginatedSchema, and CARD_NOT_FOUND โ†’ 404 (was a latent 500). Front (#125): useCardSearch (paginated, keepPreviousData), an api-client cards module, and a generic isomorphic toQueryString util; api-client tests migrated to the shared MSW server. Earlier in 3.3: search data + service layer (#122), sync levels 1 & 2 (#114), OOM fix (#115/#96), SyncState heartbeat (#116). This file describes the current state only: environment, what works today, blockers, and what's next. Per-session history lives in decisions-log.md, the merged PRs, and git โ€” see also retrospectives/.


Environment โ€‹

VariableStatus
DATABASE_URLโœ… Configured (Session Pooler port 5432 โ€” wcvexyibmjkuzufbvuyh, eu-west-1)
Supabase projectโœ… Active (wcvexyibmjkuzufbvuyh.supabase.co) โ€” recreated session 21
SUPABASE_URLโœ… Configured
SUPABASE_ANON_KEYโœ… Configured (sb_publishable_* format)
SUPABASE_SERVICE_ROLE_KEYโœ… Configured (sb_secret_* format)
COOKIE_SECRETโœ… Configured (real 64-char secret)
NODE_ENVโœ… development โ€” required for secure: false cookies on localhost
Redisโœ… Dev: docker-compose.yml (localhost:6379, passwordless). Prod: internal container
REDIS_PASSWORDโœ… Prod only โ€” server ~/apps/decksmith/.env (never committed; openssl rand -hex 32)
.env.exampleโœ… Up to date (Session Pooler + COOKIE_SECRET + VITE_API_URL + NODE_ENV)

What's Working (today) โ€‹

Quality gates โ€” pnpm lint (oxlint), pnpm format:check (oxfmt), pnpm typecheck (TypeScript 7): 0 errors. pnpm test: 362 passing (30 domain ยท 76 schema ยท 56 api ยท 24 api-client ยท 21 query ยท 23 utils ยท 41 web-ui ยท 50 services ยท 14 web ยท 22 scryfall ยท 5 worker). Storybook CI runs play functions + axe on every story.

Backend โ€” Fastify API (pnpm dev:api โ†’ localhost:3000): user CRUD + all 7 auth routes (register, login, logout, refresh, forgot/reset-password, GET /me), card search โ€” four public /api/v1/cards routes (search / :id / :id/prints / autocomplete) + card-mapper, #124, Zod type provider, error handler with ServiceError exception mapper (ADR-0024; CARD_NOT_FOUND โ†’ 404), auth plugin (app.authenticate), rate limiting + CORS. All orchestration in packages/services; routes are HTTP glue.

Database โ€” Prisma 7 schema (18 models โ€” SyncState added for worker sync bookkeeping, ADR-0031; CardFace for multi-face cards, ADR-0029; Card + CardPrint gained the search-support fields of ADR-0032 โ€” rarities/finishes/firstReleasedAt and releasedAt/setName) synced to Supabase via session pooler; Supabase client working from packages/db. RLS policies applied and verified (4 policies on the authenticated role โ€” defense-in-depth, the API bypasses RLS; ADR-0022, session 25).

Frontend โ€” pnpm dev:web โ†’ localhost:3001. TanStack Start SSR: auth pages (login, register, forgot-password), _authenticated guard (beforeLoad + SSR cookie forwarding, ADR-0023), dashboard, root redirect by auth state. Cookie-based theme + language (SSR-safe, no FOUC/FOUT), i18n EN + FR from packages/i18n (ADR-0025).

Worker โ€” apps/worker (pnpm dev:worker, or pnpm dev:backend for Redis + api + worker): scryfall-card-sync job on BullMQ (daily cron, concurrency 1, idempotent retries), writing Card/CardPrint/CardFace via Prisma directly (ADR-0030). Composes the 3.1 bricks โ†’ chunkAsyncIterable โ†’ groupChunk (dedup, FK order) โ†’ upsertChunk (per-chunk transaction). Redis via docker-compose.yml (dev). One-off trigger pnpm worker:sync:once. Deployed in prod (PR #111): 4th GHCR image, internal Redis container (internal network only, requirepass + AOF + redis-data volume), depends_on: service_healthy; daily cron 06:00 UTC live and verified.

Packages โ€” tokens (tokens.css single source of truth), web-ui (~40 components + hooks, all Base UI + semantic tokens), domain (MTG color/mana logic), schema (Zod DTOs + stable error codes), scryfall (raw Scryfall Zod schemas + normalizeCard + isCollectibleCard + bulk client: getBulkDataInfo / fetchBulkStream (gzip) / streamNormalizedCards (JSONL via readline), provider knowledge), api-client (+ cards module: search/autocomplete/getCard/ getCardPrints), query (+ useCardSearch โ€” paginated, keepPreviousData), services (+ card-service: searchCards / getCardWithPrints / getCardPrints / autocompleteCards via $queryRaw + Prisma.sql, smoke-tested), test-utils, utils (+ chunkAsyncIterable, makeContainsPattern, toQueryString), i18n, db. schema also exports a toPaginated runtime builder beside makePaginatedSchema. Build pipeline compiles packages to dist/; apps/api runs compiled node dist/index.js.

Production โ€” fully deployed, see Infrastructure below.

Observability & Release โ€” API docs (Scalar) at /api/reference; error tracking via self-hosted GlitchTip (@sentry/node + @sentry/react, prod-only, no-op in dev), web stacks de-minified via source maps uploaded to GlitchTip per release; automated SemVer releases (semantic-release, ADR-0028) surfaced at GET /api/version + web footer (currently 1.8.0); external uptime (Better Stack); infra dashboard (Homepage) at dashboard.<domain>.


What's NOT Working / Blockers โ€‹

  • The 81 rows the sync skips are edge layouts (likely reversible_card โ€” oracle_id/cmc live on the faces, not top-level) that fail ScryfallCardSchema. Small (0.09%), not a bug; a follow-up may handle/filter them explicitly instead of logging them as "invalid".
  • apps/mobile is an empty shell
  • OAuth providers (Google, GitHub) not yet enabled in Supabase dashboard
  • Supabase email confirmation disabled (dev-only) โ€” re-enable before production or when the email confirmation flow is implemented
  • DB seed creates orphaned User profiles (no matching auth.users row) โ€” usable for DB exploration, not for auth testing; full fix needs supabase.auth.admin.createUser() before seeding
  • apps/api tests use mocked Prisma/Supabase โ€” real-DB CI (Docker PostgreSQL) pending, see test-strategy.md
  • Dev docker-compose now exists but Redis-only (Postgres stays on Supabase cloud); needs OrbStack/ Docker running for the worker
  • Deploy workflow actions target deprecated Node 20 โ€” bump in a future session
  • Storybook preview: brief light-theme flash on story change (cosmetic)
  • Postgres log noise: 42P01/3F000 on supabase_migrations.schema_migrations (we use Prisma db:push, not Supabase CLI migrations) โ€” to investigate

Dev gotchas โ€” after pnpm install, regenerate the Prisma client (pnpm --filter @decksmith/db db:generate); after route changes, regenerate routeTree.gen.ts (start pnpm dev:web, then Ctrl-C).


Next Up โ€‹

  • Card search UI โ€” the whole search stack now exists end-to-end (DB โ†’ service โ†’ routes โ†’ useCardSearch), so the next build is the real UI in apps/web: a /search page + the header spotlight/autocomplete (see design/screens/card-search.md and the three search patterns in DESIGN.md). Next up. Phase 3 (Scryfall) is fully complete.
  • #113 โ€” unify the duplicated MTG enums (Rarity/Color: schema Zod vs domain type) into a single source of truth (small ADR). Surfaced while adding special/bonus rarities.
  • Scryfall feature backlog โ€” survey in scryfall-capabilities.md, issues #100โ€“#108 (Tier 1: oracle tags / all_parts / edhrec_rank are the high-leverage next data wins).
  • foilโ†’finish enum (#85); GIN index on Card.keywords.
  • Phase 2.2 remainder: enable OAuth providers (Google, GitHub); email confirmation + password reset flow (blocked on OAuth/deep-link spec)
  • Consolidation backlog P1: 30-min service-layer walkthrough (retro E2)
  • Follow-up: source-map E2E validation โ€” trigger a real prod web error, confirm GlitchTip shows file:line instead of minified output

Open PRs โ€‹

  • None. #124 (card routes + toPaginated) and #125 (useCardSearch + api-client cards module + toQueryString) merged โ€” Phase 3.3 complete. Earlier: #122, #114, #115 (closes #96), #116.

Infrastructure (production) โ€‹

  • Live at decksmith.<domain>, path-routed behind Traefik: /api (API), / (web SSR), /docs (VitePress), /design-system (Storybook). Register/login verified end-to-end (same-origin cookies).
  • Traefik owns 80/443 on the VPS, label-driven routing over the shared proxy network, wildcard TLS via ACME DNS-01 (auto-renew). ADR-0026 + apps/docs/deployment/reverse-proxy.md.
  • Deploy pipeline: .github/workflows/deploy.yml builds 4 GHCR images (api / web / statics / worker) โ†’ scp deploy/compose.yml + SSH (set -euo pipefail) docker compose pull && up -d. Deployed SHA pinned as IMAGE_TAG in the server .env. Secrets: VPS_HOST, VPS_USER, VPS_SSH_KEY (dedicated ed25519 key). GHCR images public.
  • Worker + Redis (PR #111, ADR-0030/0031): the worker container (headless BullMQ consumer, daily Scryfall sync) and an internal redis container (digest-pinned, requirepass + AOF + redis-data volume, healthcheck) run on the internal network only โ€” never Traefik-routed. The worker waits on depends_on: redis service_healthy; cron 06:00 UTC. One-off: docker compose exec worker node dist/sync-once.js.
  • Server-side ~/apps/decksmith/.env (never committed): DECKSMITH_HOST, CORS_ORIGIN, DATABASE_URL, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, COOKIE_SECRET, REDIS_PASSWORD, IMAGE_TAG. NODE_ENV unset โ†’ production (secure cookies, trustProxy).
  • DNS: wildcard record โ†’ VPS via API-capable provider. Traefik dashboard behind IP-allowlist + basic auth. Infra secrets live only in server-side ~/infra/.env.
  • Observability: self-hosted GlitchTip (error tracking) at monitoring.<domain> in ~/infra/glitchtip/; Better Stack (external uptime) on /api/health + web root; Homepage infra dashboard at dashboard.<domain> in ~/infra/homepage/ (basic auth, reuses the Traefik dashboard middleware). All observability config lives on the VPS, never in the repo.
  • Releases: semantic-release heads the deploy pipeline (ADR-0028) โ€” git tag + GitHub Release + root package.json bump (chore(release) [skip ci]), version baked into images and surfaced at /api/version + web footer. CI secrets added: VITE_SENTRY_DSN, SENTRY_URL, SENTRY_AUTH_TOKEN.

Current Branch โ€‹

  • main โ€” #124 + #125 merged (card routes + useCardSearch). Phase 3.3 complete. Next: the card search UI in apps/web (/search page + header spotlight).

Open Decisions (not yet ADR'd) โ€‹

  • Profile completion state: what happens when a user has no username/displayName yet? A redirect to an onboarding screen is needed but not yet specced.

Dependency versions live in the individual package.json files โ€” never duplicated here.

Built with VitePress