This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Klura is a real-time, multiplayer digital board game platform with asymmetric gameplay. Players join via QR code/link and receive role-specific interfaces based on their assigned persona (e.g., Detective vs Murderer in a Murder Mystery game).
Current Status: Greenfield - Architecture designed, project structure created, implementation in progress.
Golden Rule: Mutations go through tRPC. Updates come through Socket.IO.
Client Action → tRPC (validates) → Backend Service (processes) → Redis (updates state)
↓
Client UI ← Socket.IO (broadcasts) ← GameGateway (emits GAME_UPDATE) ←┘
Constraints:
- Client NEVER calculates game logic (dice rolls, move validation, etc.)
- tRPC mutations return
{ success: true }, NOT the new game state - Game state arrives via Socket.IO
GAME_UPDATEevent to ensure all clients sync - Client uses Socket.IO for LISTENING only (no client→server emits except heartbeat)
The system scales to 100s of games via a 3-tier content architecture:
Tier 1: Game Templates → Reusable mechanics (murder-mystery-engine)
Tier 2: Game Variants → Themed instances (murder-mystery-mansion)
Tier 3: Story Definitions → Swappable narratives (blackwood-narrative)
Key Principle: Game content is data (JSON/YAML), NOT code. New games are added via configuration files in /game-content/, not new NestJS modules.
Core Engine vs Plugins:
/apps/backend/src/core/= Game-agnostic logic (game-engine, persona-engine, phase-engine, action-engine, content-loader)/apps/backend/src/plugins/= Game-specific mechanics (murder-mystery.plugin.ts implements custom actions like ACCUSE_PLAYER)
Content Location:
- Development:
/game-content/directory (JSON/YAML files) - Production: PostgreSQL (loaded via
GameDefinitionService, cached in-memory for 30min)
pnpm dev # Start Vite dev server
pnpm build # Type-check with vue-tsc, then build
pnpm preview # Preview production buildNote: Backend is not yet initialized. When implemented:
pnpm dev # Start NestJS dev server with hot reload
pnpm build # Build TypeScript
pnpm test # Run unit tests (Vitest/Jest)
pnpm test:e2e # Run end-to-end testsWhen implemented:
pnpm build # Compile TypeScript, generate type definitions
pnpm test # Run schema validation tests/apps/
├── backend/ # NestJS + Fastify + tRPC + Socket.IO
│ └── src/
│ ├── core/ # Game-agnostic engine components
│ ├── plugins/ # Game-specific mechanics
│ ├── admin/ # Content Management API
│ └── modules/ # tRPC routers, auth, websocket
│
└── klura-front-end/ # Vue 3 + Vite + Pinia + Socket.IO-client
/packages/
└── shared/ # Shared contracts (Zod schemas, TS types, themes)
├── schemas/ # Runtime validation (Zod)
├── types/ # Compile-time types (TypeScript)
└── themes/ # Game theming (colors, fonts, mood)
/game-content/ # Game definitions (JSON/YAML)
├── templates/ # Reusable game mechanics
├── variants/ # Themed game instances
└── stories/ # Narrative content
When a player performs an action:
- Client calls
trpc.game.action.mutate({ roomId, actionType: 'MOVE_PLAYER', payload }) - tRPC Router validates input via Zod schema
- GameEngineService orchestrates:
- Fetch current state from Redis
- Load game config via
GameDefinitionService - Delegate to
ActionValidatorService(checks if action allowed) - Delegate to
PluginRegistry(finds game-specific handler) - Execute action via plugin's
customActions['MOVE_PLAYER'] - Update state via
GameStateManager - Save to Redis
- GameGateway emits
GAME_UPDATEvia Socket.IO to room - PersonaEngine filters state per player (players only see info their persona allows)
- All Clients receive filtered state, update Pinia store, UI re-renders
Component (Presentation Only)
↓ uses
Composable (e.g., useGameSession)
↓ calls
tRPC Client (type-safe API calls)
↓ listens to
Socket.IO Client (state updates)
↓ updates
Pinia Store (reactive state)
↓ feeds back to
Component (via computed properties)
Critical: Vue components MUST NOT contain business logic. All logic goes in composables/services/stores.
All data structures are defined here FIRST (Contract-First Development):
-
Define Zod Schema (runtime validation):
// schemas/game-variant.schema.ts export const GameVariantSchema = z.object({ id: z.string().min(1), templateId: z.string(), personas: z.array(PersonaDefinitionSchema).min(2), // ... });
-
Infer TypeScript Type:
// types/game.types.ts export type GameVariant = z.infer<typeof GameVariantSchema>;
-
Use in Backend (validate with Zod):
const validatedInput = GameVariantSchema.parse(input);
-
Use in Frontend (type-safe via tRPC):
const { data } = trpc.game.getVariant.useQuery({ variantId }); // data is fully typed as GameVariant
Rule: any type is FORBIDDEN in /packages/shared/.
- Write failing test (Red)
- Implement minimal code to pass (Green)
- Refactor for quality (Refactor)
- Minimum 80% test coverage for all new code
- Break work into atomic tasks (few hours each)
- Commit after EVERY completed task
- Update relevant documentation with changes
- TypeScript strict mode REQUIRED (
strict: truein tsconfig) - Avoid
any; useunknownif truly uncertain - Explicitly declare return types for all functions
TypeScript:
- Interfaces:
PascalCase(noIprefix) - Variables/Functions:
camelCase - Constants:
UPPER_SNAKE_CASE - Prefer
constoverlet - Use
readonlyfor immutable properties - Early returns to reduce nesting
async/awaitover.then()chains
Vue:
- Always use
<script setup lang="ts"> - Components:
PascalCase(e.g.,GameCard.vue) - Props:
camelCasein code,kebab-casein templates - Events:
emit('game-started') - Unique
:keyin allv-forloops
Server MUST filter game state before broadcast. Players only receive information their persona can see.
Example: In Murder Mystery, the murderer's identity is in state.privateData.murdererId. This must be:
- Sent to the murderer
- Hidden from all other players
Implement via PersonaEngine.filterStateForPersona().
- Socket disconnects trigger "Reconnecting..." overlay (blocks user input)
- On reconnect: call
trpc.game.getState()to fetch authoritative state - Grace period: 60s before marking player disconnected
- Rejoin window: 10 minutes to rejoin in-progress game
- Heartbeat: Client sends every 30s; server times out after 90s
- All game state stored in Redis with key pattern:
game:{roomId} - TTL: 24 hours (prevents memory leaks from abandoned games)
- On game end: archive final state to PostgreSQL, delete Redis key
Each game defines its own theme in /packages/shared/themes/:
interface GameTheme {
id: string;
colors: { primary, secondary, background, surface, text, accent, danger, success };
fonts: { heading, body, mono };
mood: 'light' | 'dark' | 'dramatic';
backgroundImage?: string;
sounds?: { turnStart, actionConfirm, reveal };
}Implementation:
- Vue provide/inject at app root
- CSS custom properties for runtime switching
- Tailwind plugin reads theme tokens
- 300ms animated transitions on theme change
/conductor/code_styleguides/architecture.md- Comprehensive scalability architecture/apps/backend/src/core/README.md- Core engine components/apps/backend/src/plugins/README.md- Plugin system guide/apps/backend/src/admin/README.md- Content Management API/game-content/README.md- Content structure and examples/packages/shared/README.md- Shared types and schemas/initial.md- Command & Sync pattern deep dive/conductor/tech-stack.md- Technology decisions and API contracts/PROJECT_STRUCTURE.md- Complete project overview
game.create- Host creates lobbygame.join- Player joins via QR/linkgame.start- Host starts gamegame.getState- Fetch current state (reconnect/refresh)game.action- Player performs actiongame.leave- Player exits
All mutations return { success: true } on success, throw error on failure.
GAME_UPDATE- Full/partial state syncPLAYER_JOINED/PLAYER_LEFT- Presence updatesGAME_STARTED/GAME_ENDED- Lifecycle eventsTURN_CHANGED- Active player rotationPERSONA_REVEALED- Private: sent only to target playerPHASE_CHANGED- Game phase transitionsERROR- Server error notification
Phase 1 (MVP - Current): Hardcoded Murder Mystery module in /apps/backend/src/modules/murder-mystery/
Phase 2: Extract to JSON config, implement GameDefinitionService
Phase 3: Persona & story abstraction, visibility filtering
Phase 4: Plugin system, refactor Murder Mystery to plugin
Phase 5: Admin API for content management, hot-reload
- Frontend: Vue 3, Vite, Tailwind CSS, Pinia, socket.io-client
- Backend: NestJS (Fastify), tRPC, Socket.IO, TypeScript
- Database: Redis (hot), PostgreSQL/Supabase (cold)
- Validation: Zod (runtime), TypeScript (compile-time)
- Monorepo: pnpm workspaces