Real-time multiplayer debugging game built as an npm workspaces monorepo.
Players join a room, get rotating roles (proposer/counter/voter), answer code-bug questions under time limits, and score points based on correctness and voting outcomes.
.
├─ apps/
│ ├─ web/ # React + Vite + Tailwind client
│ └─ server/ # Socket.IO + Redis game server
├─ packages/
│ └─ shared/ # Shared zod schemas + typed socket event contracts
├─ .env.example # Reference values for local env variables
└─ package.json # Workspace scripts
- Frontend: React 19, TypeScript, Vite 7, Tailwind CSS 4
- Backend: Node.js, TypeScript (tsx runtime), Socket.IO, ioredis
- Shared contracts: zod schemas + TypeScript types in
@debugrush/shared - State store: Redis (
room:*keys, TTL-based cleanup)
- Room IDs are exactly 6 uppercase alphanumeric characters.
- Max connected players per room: 5.
- Minimum players to start: 3 connected players.
- Roles rotate round-robin by join order:
proposerpicks an option (+ optional reason)counterpicks an option (+ optional reason)- remaining connected players vote (
proposerorcounter)
- During vote phase, active voters also get a voter-only realtime chat channel.
- Majority vote determines final side automatically.
- If selected side is wrong, game ends immediately.
- Reveal phase shows outcome, then next round starts (or game ends).
propose: 40scounter: 30svote: 50sreveal: 10sfinal: 12s exists in schema/engine for compatibility, but final decision is currently automatic from majority vote.
- Proposer/counter manual pick:
- correct:
+4 - wrong:
-4
- correct:
- Voter manual vote:
- correct side:
+2 - wrong side:
-2
- correct side:
- Tie round mode:
- correct:
+1 - wrong:
-1
- correct:
- Timeout auto-pick penalty for proposer/counter:
-2(fixed override for that role action)
Shared event typing lives in packages/shared/src/events.ts.
Client -> Server:
auth:whoamiroom:joinroom:leavegame:startround:proposer:submitround:counter:submitround:vote:submitround:vote:chat:sendround:final:submit(currently disabled by server)round:reveal:skip
Server -> Client:
auth:identityroom:stateroom:leftround:vote:chat:messageaction:error
All payloads are zod-validated in packages/shared/src/schemas.ts.
- Room state is stored in Redis and refreshed with TTL on mutation.
- TTL: 2 hours (
ROOM_TTL_SECONDSinapps/server/src/repo/roomsRepo.ts). - Atomic room mutations use Redis
WATCH/MULTIwith retries to reduce race conditions. - On server boot, in-progress rooms are scanned and phase timers are recovered.
- Disconnect handling:
- 1.5s grace window allows refresh/reconnect without immediate removal.
- if a player actually leaves during an active match, game ends immediately.
- Node.js 20+ (recommended)
- npm 10+ (recommended)
- Redis running locally (default:
redis://localhost:6379)
npm workspace scripts run each app in its own directory, so use per-app .env files:
apps/server/.env
REDIS_URL=redis://localhost:6379
PORT=4000
CORS_ORIGIN=http://localhost:5173
# Optional: allow client-provided socket identity in production
# if session/token auth is not integrated yet.
ALLOW_UNVERIFIED_CLIENT_IDENTITY=falseapps/web/.env
# Required in non-development builds.
# In development, empty falls back to http://localhost:4000 in client code.
VITE_WS_URL=http://localhost:4000Notes:
.env.examplein repo root is a reference template only.- If Vite starts on a different port (for example
5174), updateCORS_ORIGINaccordingly or free5173.
- Install dependencies:
npm install- Start Redis (example with local install):
redis-server- Start web + server together:
npm run dev- Open the client:
http://localhost:5173
If 5173 is already in use, Vite may auto-switch ports. In that case, make sure server CORS_ORIGIN matches the actual web origin.
Root scripts (package.json):
npm run dev-> kills port4000, then runs web + server concurrentlynpm run dev:web-> starts onlyapps/webnpm run dev:server-> starts onlyapps/servernpm run build-> workspace build (currently fails becauseapps/serverhas nobuildscript)npm run lint-> workspace lint
Workspace scripts:
- Web (
apps/web/package.json)npm run dev -w apps/webnpm run build -w apps/webnpm run preview -w apps/web
- Server (
apps/server/package.json)npm run dev -w apps/server
- Shared (
packages/shared/package.json)npm run build -w packages/shared
- No automated tests are configured yet.
apps/serverhas nobuildscript, so rootnpm run buildexits with a missing-script error for that workspace.- Root
npm run lintcurrently reports existing lint issues inapps/web. - Production identity verification middleware is not wired in this repo yet:
- in production mode, server requires authenticated identity from session or verified token.
- development fallback uses client-provided handshake identity.
- Question deck is defined in
apps/server/src/engine/gameEngine.ts(QUESTION_DECK). - Current deck contains 98 active questions (no commented-out deck entries), so rounds rotate through a broad question pool.
- Duplicate proposer/counter pick behavior:
- server generates a system alternative option for voting.
- vote resolution gives baseline support to the shared role pick to prevent a single voter from overriding both role picks.
- Vote chat behavior:
- only active voters can send/read chat messages
- chat is available only during
votephase
- Immediate game-over conditions include:
- both proposer and counter pick different wrong options.
- a player leaves/disconnects during an active match.
Missing REDIS_URLon server start:- create/fix
apps/server/.envand ensure Redis is running.
- create/fix
- Web cannot connect to server:
- verify
VITE_WS_URL, serverPORT, andCORS_ORIGINalignment.
- verify
- Frequent room errors under heavy multi-client actions:
- expected occasional
ROOM_BUSYretries due optimistic Redis transactions.
- expected occasional
- Stale favicon/tab icon in browser:
- do a hard refresh or clear site cache (favicons are aggressively cached).
- Add
apps/serverbuild/start scripts for production packaging. - Add end-to-end tests for round transitions and scoring.
- Move question deck to durable storage + admin tooling.
- Add proper auth middleware for production identity.