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 โ
| Variable | Status |
|---|---|
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/cmclive on the faces, not top-level) that failScryfallCardSchema. Small (0.09%), not a bug; a follow-up may handle/filter them explicitly instead of logging them as "invalid". apps/mobileis 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
Userprofiles (no matchingauth.usersrow) โ usable for DB exploration, not for auth testing; full fix needssupabase.auth.admin.createUser()before seeding apps/apitests use mocked Prisma/Supabase โ real-DB CI (Docker PostgreSQL) pending, seetest-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/3F000onsupabase_migrations.schema_migrations(we use Prismadb: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 inapps/web: a/searchpage + the header spotlight/autocomplete (seedesign/screens/card-search.mdand the three search patterns inDESIGN.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 addingspecial/bonusrarities. - 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โfinishenum (#85); GIN index onCard.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:lineinstead 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
proxynetwork, wildcard TLS via ACME DNS-01 (auto-renew). ADR-0026 +apps/docs/deployment/reverse-proxy.md. - Deploy pipeline:
.github/workflows/deploy.ymlbuilds 4 GHCR images (api / web / statics / worker) โ scpdeploy/compose.yml+ SSH (set -euo pipefail)docker compose pull && up -d. Deployed SHA pinned asIMAGE_TAGin 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
workercontainer (headless BullMQ consumer, daily Scryfall sync) and an internalrediscontainer (digest-pinned, requirepass + AOF +redis-datavolume, healthcheck) run on theinternalnetwork only โ never Traefik-routed. The worker waits ondepends_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_ENVunset โ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 atdashboard.<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.jsonbump (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 inapps/web(/searchpage + header spotlight).
Open Decisions (not yet ADR'd) โ
- Profile completion state: what happens when a user has no
username/displayNameyet? A redirect to an onboarding screen is needed but not yet specced.
Dependency versions live in the individual
package.jsonfiles โ never duplicated here.