Deterministic turn-based game stack in a TypeScript monorepo: authoritative rules engine, Fastify + WebSocket server, and Vite + React client.
Follow active progress and implementation notes in the Developer Log.
- Packages
- Getting Started
- Build
- Tests
- Environment Variables (Web)
- Assets (Figure Arts + Tokens)
- Server API
- WebSocket
- Deployment
- Common Pitfalls
- Quick Verify
- Notes
- Developer Log
| Package | Path | Purpose |
|---|---|---|
rules |
packages/rules |
Deterministic game engine (authoritative state updates). |
server |
packages/server |
Fastify + WebSocket game server. |
web |
packages/web |
React client UI. |
npm installnpm run devThis runs:
rulesin TypeScript watch mode (emitsdist/)serveronhttp://localhost:3000webonhttp://localhost:5173
Note: the server waits for packages/rules/dist/index.js before starting to avoid intermittent Cannot find module ... rules/dist/index.js crashes during watch startup.
Optional local env file (not required for dev):
- Copy
.env.exampleto.envif you want to pin API/WS URLs.
- Open
http://localhost:5173 - Create a room in the Lobby and copy the room id
- Join as P1 in the first tab
- Open a second tab and join the same room as P2 (or spectator)
npm run buildnpm run test
npm run -w web typecheckThe frontend reads these at build time. In production builds they are required and the build will fail fast if missing:
VITE_API_URL(example:https://your-render-app.onrender.com)VITE_WS_URL(example:wss://your-render-app.onrender.com/ws)VITE_ENABLE_TEST_ROOM=true- shows the Test Room creator when the server also allows it. Local Vite development enables the entry automatically.
Local defaults are provided in .env.example.
WEB_ORIGIN- allowed browser origin for CORS and WebSocket Origin checks, for examplehttps://your-app.vercel.app.FATE_DEBUG_TOKEN- production-only token for debug REST game views/actions/logs. Send it asX-FATE-DEBUG-TOKEN: <token>.ENABLE_TEST_ROOMS=true- enables authoritative Test/Sandbox rooms. Development enables them by default unless explicitly set tofalse; production requires this flag and a matchingFATE_DEBUG_TOKENwhen creating the room.ROOM_TTL_MS- idle room TTL before cleanup. Default:86400000(24 hours).MAX_ROOMS- maximum in-memory FATE rooms retained. Default:100.MAX_LOG_EVENTS- maximum action log entries retained per room. Default:5000.WS_MAX_PAYLOAD_BYTES,WS_RATE_LIMIT_WINDOW_MS,WS_RATE_LIMIT_MAX_MESSAGES,RECONNECT_GRACE_MS- WebSocket payload, rate, and reconnect controls.
Local development keeps debug REST endpoints open when NODE_ENV !== "production". In production, GET /api/games/:id, POST /api/games/:id/actions, and GET /api/games/:id/log require X-FATE-DEBUG-TOKEN to match FATE_DEBUG_TOKEN. Browser WebSocket connections must use an allowed Origin; missing Origin is accepted for non-browser clients.
Test rooms are server-authoritative manual QA rooms. A single controller can spawn catalog units for either side, edit HP/statuses/charges, force turns and phases, run normal attacks and abilities, queue deterministic d6 results, place stakes/forest markers, inspect pending rolls/state, load deterministic presets, and export/import bounded JSON snapshots.
Enable locally:
VITE_ENABLE_TEST_ROOM=true
ENABLE_TEST_ROOMS=trueIn production, ENABLE_TEST_ROOMS=true is required and room creation must include the configured FATE_DEBUG_TOKEN. Sandbox commands are rejected for normal rooms, disabled deployments, spectators, and non-controller connections. Imported snapshots are capped and validated before replacing test-room state.
Recommended formats:
- WEBP preferred (best size/quality)
- PNG allowed (if transparency is needed)
- Token transparency: use WEBP/PNG with alpha
Suggested sizes:
- Full art:
1024x1536(2:3) or1200x1800 - Token:
256x256or512x512square
File naming rules (must match figureId):
packages/web/src/assets/figures/<figureId>.webppackages/web/src/assets/tokens/<figureId>.webp
Tips:
- Try to keep full art under ~300-600 KB if possible
- Assets under
src/assetsare bundled by Vite; if user uploads are needed later, move them to/publicor external storage
GET /- basic server infoGET /health- health checkGET /api/health- health check (legacy)POST /api/games- create a game- body:
{ "seed"?: number, "arenaId"?: string }
- body:
GET /api/games/:id?playerId=P1|P2- debug player-specific state view; production requiresX-FATE-DEBUG-TOKENPOST /api/games/:id/actions?playerId=P1|P2- debugGameActionsubmit; production requiresX-FATE-DEBUG-TOKENGET /api/games/:id/log- debug action log; production requiresX-FATE-DEBUG-TOKENGET /rooms- list room summariesPOST /rooms- create a room (returnsroomId)
GET /ws- client -> server:
{ type: "joinRoom", roomId, requestedRole, name? }{ type: "leaveRoom" }{ type: "action", action }{ type: "requestMoveOptions", unitId }
- server -> client:
{ type: "joinAccepted", roomId, role, connId }{ type: "joinRejected", reason, message }{ type: "roomState", roomId, room }{ type: "actionResult", ok, events, error?, logIndex? }{ type: "moveOptions", unitId, roll, legalTo }{ type: "error", message }
- client -> server:
Deployment updates are also posted in the Developer Log.
Render can build from the repo root.
- Build command:
npm install && npm run -w rules build && npm run -w server build - Start command:
npm run -w server start
Environment variables:
PORT(Render sets this automatically)WEB_ORIGIN(set to your Vercel URL for CORS and WebSocket Origin checks)FATE_DEBUG_TOKEN(required only if using debug REST endpoints in production)ROOM_TTL_MS,MAX_ROOMS,MAX_LOG_EVENTS(optional in-memory bounds)NODE_VERSION=22(optional)
Notes:
- The server binds to
0.0.0.0and usesPORT - WebSockets are available at
wss://<render-host>/ws
Recommended Vercel settings:
- Framework preset: Vite
- Root Directory:
packages/web - Install Command:
npm install --prefix ../.. - Build Command:
cd ../.. && npm run -w web build - Output Directory:
dist
Environment variables (required for production builds):
VITE_API_URL=https://<render-server>.onrender.comVITE_WS_URL=wss://<render-server>.onrender.com/ws
Failed to fetchin production usually meansVITE_API_URLpoints to localhost- WS connection failures usually mean
VITE_WS_URLshould bewss://.../wsin production - CORS errors usually mean
WEB_ORIGINis missing or incorrect on Render
- Open the Vercel site, check Network tab:
/roomsshould hit your Render domain - Confirm WebSocket connects successfully (
wss://<render-host>/ws) - Visit
https://<render-host>/healthand see{ ok: true }
- The server is authoritative: the client only sends
GameActionintents - Per-player visibility is enforced by
makePlayerView(exported fromrules) - Avoid running Vite with
--host 0.0.0.0unless you explicitly need LAN access
Track implementation updates, architecture notes, and release progress in the Telegram channel: