Reconstructed from /root/briggen-dev/ttrpg — the tree the running prod
container was composed from. Forked from 235cdec (2026-06-08); adds 3D
dice + themes, extended dice notation, session replay, route-level code
splitting, and more.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A local-first PWA campaign manager for D&D 5e and Pathfinder 2e. Primary data lives in
IndexedDB on the device; the app works fully offline with no account. On top of that there is an
optional Fastify server (server/) that adds three networked features: live multiplayer
sessions (WebSocket), encrypted-blob cloud backup, and shared cloud campaigns. The browser app and
the server share TypeScript modules (notably src/lib/sync/messages.ts) via the @ alias.
The package manager is bun. The README's "no server" framing describes the default offline mode;
the server and realtime sync are implemented (see server/ and src/lib/sync/wsSync.ts).
Commands
bun install
bun run dev # vite dev server at http://localhost:5173
bun run build # tsc -b (typecheck) + vite build → dist/
bun run build:server # esbuild bundle → server/dist/index.js
bun run lint # eslint
bun run typecheck # tsc -b --noEmit (client)
bun run typecheck:server # tsc --noEmit -p tsconfig.server.json
bun run test # vitest run (all unit tests, jsdom)
bun run test:watch
bunx vitest run path/to/file.test.ts # single unit test file
bunx vitest run -t "name of test" # single test by name
bun run test:e2e # playwright against dev server (e2e/)
bunx playwright test e2e/combat-depth.spec.ts # single e2e file
Realtime e2e runs against the built server (one origin serving dist/ + /ws):
bun run build && bun run build:server
bunx playwright test -c playwright.realtime.config.ts # e2e-realtime/
Architecture
The rules-system seam (most important)
src/lib/rules/types.ts defines the RulesSystem interface — the single contract for all
system-specific math (ability mods, skills, saves, AC, proficiency, spell DCs, weapon attacks,
rest options, carrying capacity). 5e and pf2e each implement it under src/lib/rules/dnd5e/
and src/lib/rules/pf2e/. Get an implementation via getSystem(systemId).
Feature and store code must never branch on if (system === '5e'). Call the RulesSystem
methods instead. Adding a new system should be a self-contained module that implements the interface.
Data layer: schemas → repositories → Dexie
src/lib/schemas/— Zod schemas for every entity. Numbers rejectNaN/Infinity. This is the validation boundary; entities are parsed on write.src/lib/db/repositories.ts— the only way features touch the database. Repos validate with Zod on write, stampupdatedAt, and do cascade deletes inside Dexie transactions (deleting a campaign atomically removes all its children). Do not calldb.*tables directly from features.src/lib/db/db.ts— the single Dexie instance. Schema versions are strictly additive: to change shape, bumpversion(n)and add an.upgrade()migration that backfills — never edit a pastversion()block in place. New optional fields need no migration (rows still parse).
These choices exist to kill the failure modes of the previous (deleted) implementation, catalogued
in docs/OLD_BUG_HUNT_REPORT.md: silent data loss, turn-order corruption, missing cascade deletes,
NaN inputs.
Pure engines
src/lib/combat/ (turn-order + HP), src/lib/dice/ (notation parser + seedable roller), and the
rules math are pure and unit-tested. Add/remove/reorder combatants must never corrupt whose turn it
is — preserve that invariant and its tests.
State & UI
src/stores/— Zustand stores;persistmiddleware for state that must survive reload (e.g.sessionStore's join intent). Dexie data is read reactively viadexie-react-hooksliveQuery, not duplicated into stores.src/features/<feature>/— UI per domain (campaigns, characters, combat, dice, compendium, world, play, player, assistant, cloud, settings).src/components/ui/holds design-system primitives.- Routing is code-defined in
src/router.tsx(TanStack Router) — add a route there. src/lib/compendium/lazy-loads SRD JSON fromsrc/data/srd/so it stays out of the main bundle.
Sync: two seams
src/lib/sync/index.ts—SyncAdapter.localSyncis a no-op (same-device sharing is automatic vialiveQuery).src/lib/sync/wsSync.ts— the real WebSocket transport for live sessions: the GM hosts and pushes player-safe snapshots (snapshot.tsprojects only player-visible state, e.g. fog, GM-only tokens, hidden HP), players join read-only, with reconnect/backoff. Message shapes are Zod schemas inmessages.ts, shared with the server.
Server (server/src/)
Fastify app (index.ts) bundled by esbuild (server/build.mjs). In production it also serves the
built PWA from STATIC_DIR. Three concerns, each its own module + test:
rooms.ts (live-session room hub over /ws), accounts.ts (auth + per-user cloud backup blob),
campaigns.ts (shared cloud campaigns with invite codes / roles). Config is env-driven: PORT,
STATIC_DIR, DATA_DIR, ALLOWED_ORIGINS, ADMIN_USERS. There is per-IP and per-socket rate
limiting. Client-side counterparts live in src/lib/cloud/.
LLM assistant (bring-your-own-key)
src/lib/llm/client.ts calls the user's configured provider (Anthropic/OpenAI/OpenRouter/local
Ollama/LM Studio) directly from the browser — robust JSON extraction handles fenced/partial replies.
src/lib/assistant/ builds prompts/context and post-processes (advisors, level-up, encounter
suggestions). The build-time CSP in vite.config.ts deliberately allows connect-src https: +
localhost so these calls work; keep that in mind when touching CSP.
Deployment
Dockerfile is multi-stage (bun build → slim node runtime, non-root, serves dist + /ws on one
port). docker-compose.yml deploys to ttrpg.briggen.dev behind an external Traefik proxy
network. Cloud-sync data persists in the ttrpg-data volume at DATA_DIR=/data.
Conventions
- TypeScript strict; import app code via the
@/alias (maps tosrc/), used by both client and the server bundle. - SRD data in
src/data/srd/is generated, not hand-edited — regenerate with thescripts/scrapers (fetch_data.py,fetch_open5e.ts,fetch_pf2e.ts).