Skip to content

Stack & infrastructure ​

  • Monorepo, pnpm workspaces.
    • apps/api — NestJS (TypeScript).
    • apps/web — React (TypeScript).
    • packages/* — shared code (domain types, the curated Mystic Forge dataset) when sharing is real, not speculative.
  • Postgres for persistence. In-memory cache for the MVP — no Redis until the caching story earns it.
  • Vitest everywhere, both apps.
  • Deploy: managed PaaS (Fly.io / Railway). CI: GitHub Actions — lint + typecheck + test + build.

Caching & external API posture ​

  • Static data (items, station recipes) is immutable: cache hard.
  • Prices are volatile: short TTL, recomputed live.
  • GW2 API rate limit: per-IP token bucket, 300 burst, refill 5/sec, 429 on overflow. Batch up to 200 ids per ?ids= call.
  • All GW2 API access goes through the client that budgets this. Never call the GW2 API directly from a service.
  • API keys are user secrets: never logged, never persisted server-side, never returned to the client. In the MVP the key is held client-side — the browser's localStorage — and sent per request as Authorization: Bearer; the api forwards it to GW2 and stores nothing at rest. Encryption at rest applies only if/when server-side key storage is introduced; no such storage exists today. Client-side localStorage is plaintext and readable by any script on the origin (XSS) — a deliberate MVP limitation, established by spec 016 (client-custody, revisit before adding write-scoped or higher-value keys).

The api build model ​

Established by spec 004 (research.md V1, F9). apps/api (NestJS on Fastify) is compiled by the SWC CLI with an explicit .swcrc (jsc.transform.legacyDecorator + decoratorMetadata), not tsc — TypeScript 7 typechecks the decorators but --noEmit never emits runtime metadata, and Nest DI reads that metadata via reflect-metadata at runtime. This is a documented, scoped divergence from the root's "Node runs TypeScript directly, no build" model (monorepo.md): the api is the one part of the repo with a real build step (swc src -d dist --strip-leading-paths), because Nest DI needs emitted decorator metadata that a typecheck-only compiler cannot produce. Use the swc CLI directly, not nest build -b swc — its internal pnpm install deps-check trips pnpm's build-approval gate below.

Two packages must be present for this to boot at all: @swc/core (a native binary; add to pnpm-workspace.yaml's allowBuilds or pnpm install/build fails under strictDepBuilds) and @fastify/static (Swagger UI's static assets are served through it on the Fastify adapter; without it the app process.exit(1)s on boot).

The contract pipeline ​

Established by spec 004 (research.md V2, V3, F10). The api is the source of truth for the HTTP contract between it and apps/web, one direction only:

Zod schema → nestjs-zod v5 (route decorated with @ZodResponse({ status, type }); the document built with cleanupOpenApiDoc(SwaggerModule.createDocument(app, config))) → a committed apps/api/openapi.json → Orval, two output entries against that one file (client: 'react-query' for typed TanStack Query hooks, client: 'zod' for standalone Zod validators — Orval does not wire the two together itself) → apps/web/src/api, a hand-written facade that composes each generated hook with its matching generated validator so components get runtime-checked data. Application code imports only from src/api, never from src/api/generated directly.

In dev, Vite's server proxy maps /api/* to the running api and strips the prefix; the generated client uses baseUrl: '/api' while the api itself serves plain, unprefixed paths (e.g. /health).

A verify:contract script guards against drift: regenerate (generate:openapi then generate:api), run pnpm format (Biome's committed style differs byte-for-byte from a raw JSON.stringify/codegen emit), then git diff --exit-code the generated files — CI runs this so a hand-edited contract artifact is caught rather than silently diverging from the api. It runs as its own CI step, not wrapped in a test: a test whose body shells out to the same script bought nothing and doubled the build (spec 004's P3 #2 amendment). tests/ci/workflow.test.ts asserts the step's presence and position; the step itself does the regenerating.

Out of scope for the MVP (decided) ​

Kubernetes, chaos engineering, load testing, WebSocket live updates, Redis, and LLM features in the product itself. The LLM layer (fuzzy goal parsing) is an optional later vertical, not the point. Do not reintroduce any of these without a spec.