Files
NilsBriggenandClaude Fable 5 1420f4d0c5 Server-live fork as deployed to ttrpg.briggen.dev (built 2026-06-30)
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>
2026-07-16 22:33:08 +02:00

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 reject NaN/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, stamp updatedAt, and do cascade deletes inside Dexie transactions (deleting a campaign atomically removes all its children). Do not call db.* tables directly from features.
  • src/lib/db/db.ts — the single Dexie instance. Schema versions are strictly additive: to change shape, bump version(n) and add an .upgrade() migration that backfills — never edit a past version() 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; persist middleware for state that must survive reload (e.g. sessionStore's join intent). Dexie data is read reactively via dexie-react-hooks liveQuery, 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 from src/data/srd/ so it stays out of the main bundle.

Sync: two seams

  • src/lib/sync/index.ts — SyncAdapter. localSync is a no-op (same-device sharing is automatic via liveQuery).
  • src/lib/sync/wsSync.ts — the real WebSocket transport for live sessions: the GM hosts and pushes player-safe snapshots (snapshot.ts projects only player-visible state, e.g. fog, GM-only tokens, hidden HP), players join read-only, with reconnect/backoff. Message shapes are Zod schemas in messages.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 to src/), used by both client and the server bundle.
  • SRD data in src/data/srd/ is generated, not hand-edited — regenerate with the scripts/ scrapers (fetch_data.py, fetch_open5e.ts, fetch_pf2e.ts).