Project State โ
Updated: 2026-08-12 (Phase 3.3 Card API โ sync levels 1 & 2 done and merged (#114). Level 2 = a post-load SQL pass (aggregateCardAttributes) recomputing Card.rarities/finishes/ firstReleasedAt from the prints; backfilled. Prod OOM incident resolved (#115, #96 closed): the per-row sync upsert leaked memory and crash-looped the worker โ replaced by a bulk INSERT โฆ ON CONFLICT per table (3m13s / <512 MB vs ~15 min + OOM); prod redeployed and verified healthy. SyncState observability (lastCheckedAt + startedAt) merged (#116, released 1.9.0; db:push applied). Remaining in 3.3: the endpoints. 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: 278 passing (30 domain ยท 47 schema ยท 34 api ยท 19 api-client ยท 18 query ยท 11 utils ยท 41 web-ui ยท 37 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), Zod type provider, error handler with ServiceError exception mapper (ADR-0024), 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, query, services, test-utils, utils (+ chunkAsyncIterable), i18n, db. 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
packages/queryhas nouseCardSearchโ blocked on Phase 3 (Scryfall)- 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 โ
- Phase 3.3 endpoints โ
GET /cards/search(+CardSearchQuerySchema: filters/sort/pagination)/cards/:id+/cards/:id/prints+ autocomplete; Postgres indexing (generatedtsvector+ GIN, array GIN, prefix/trigram) in raw SQL;useCardSearchinpackages/query(unblocks Phase 4.3). Next up.
- #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. #114 (Phase 3.3 sync levels 1 & 2), #115 (bulk-upsert OOM fix, closes #96) and #116 (
SyncStateheartbeat, released 1.9.0) all merged.
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โ #116 merged (released 1.9.0). Next: the Phase 3.3 Card API endpoints.
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.