lila/documentation/ai-context/01-architecture.md
2026-05-16 01:59:43 +02:00

6.1 KiB
Raw Permalink Blame History

01 — Architecture

Purpose: Give an LLM the structural context needed to navigate the codebase and understand data flow. Concatenate with 00-project-overview.md and 99-current-task.md. Last updated: 2026-05-15 Depends on: 00-project-overview.md


Monorepo Boundaries

lila/
├── apps/
│   ├── api/              — Express backend: routers, controllers, services, WS handlers
│   └── web/              — React frontend: routes, components, hooks, client state
├── packages/
│   ├── shared/           — Zod schemas, constants, derived types. THE CONTRACT.
│   └── db/               — Drizzle schema, migrations, models (termModel, lobbyModel), seeding
├── data-pipeline/        — Kaikki extraction → enrichment → sync to PostgreSQL
└── documentation/        — Human docs + ai-context/

Critical rule: apps/api never imports drizzle-orm for queries. It only calls functions exported from packages/db. All database code lives in packages/db.


Layered Architecture (HTTP)

HTTP Request
     ↓
  Router        — maps URL + method to controller (Express Router)
     ↓
 Controller     — validates input (Zod safeParse), calls service, sends response
                  or next(error) for errorHandler middleware
     ↓
  Service       — business logic only. No HTTP, no direct DB access.
                  Calls model functions from packages/db.
     ↓
  Model         — database queries only. No business logic.
                  Lives in packages/db/src/models/
     ↓
  Database      — PostgreSQL via Drizzle ORM

Error flow: Controller throws ValidationError (400) or NotFoundError (404) → caught by errorHandler middleware in app.ts → mapped to HTTP status. Unknown errors → 500.


WebSocket Architecture

The WS server attaches to the same Express HTTP server. Upgrades on /ws path.

WS Connection Upgrade
     ↓
Auth middleware — validates Better Auth session from cookie on upgrade
     ↓
Message Router — dispatches by `type` field (Zod discriminated union)
     ↓
Handler (lobby or game) — business logic, broadcasts state to room
     ↓
In-memory stores (lobby game state, game session state)

Message protocol: All WS messages validated against Zod schemas in packages/shared/src/schemas/lobby.ts and packages/shared/src/schemas/game.ts. Router switches on type field.

State storage:

  • Lobby membership → PostgreSQL (lobbies, lobby_players tables) — durable
  • Game/room state → in-memory (InMemoryLobbyGameStore, InMemoryGameSessionStore) — ephemeral, lost on restart. Valkey migration planned.

Data Flow: Singleplayer Quiz

POST /api/v1/game/start (GameRequestSchema)
     ↓
Controller validates → Service.createGameSession
     ↓
termModel.getGameTerms(filters) + termModel.getDistractors(filters)
     ↓
Service shuffles options, stores session in GameSessionStore
     ↓
Returns GameSession { sessionId, questions[] }
     ↓
[frontend] User selects option → confirms → POST /api/v1/game/answer
     ↓
Service evaluates server-side (correct answer NEVER sent to frontend)
     ↓
Returns AnswerResult { isCorrect, correctOptionId, selectedOptionId }

Key design: Correct answer is stored server-side only (in GameSessionStore). Frontend only sees optionId (03) and text. Prevents cheating.


Data Flow: Multiplayer Game

Host creates lobby → POST /api/v1/lobbies → returns room code (e.g. WOLF-42)
     ↓
Players join via code → POST /api/v1/lobbies/:code/join
     ↓
All players WS connect → send lobby:join with room code
     ↓
Server broadcasts lobby:state (player list) to all in room
     ↓
Host clicks "Start" → WS lobby:start
     ↓
MultiplayerGameService generates questions, broadcasts game:question
     ↓
Players submit answers via WS game:answer within 15s server timer
     ↓
On all-answered or timeout → evaluate, broadcast game:answer_result
     ↓
After N rounds → broadcast game:finished with final scores

GameSessionStore Abstraction

// packages/shared/src/schemas/game.ts (interface defined in apps/api)
interface GameSessionStore {
  createSession(session: GameSession): Promise<void>;
  getSession(sessionId: string): Promise<GameSession | null>;
  // ...
}

Current: InMemoryGameSessionStore — Map-based, process memory, lost on restart. Planned: ValkeyGameSessionStore — Redis-compatible, persists across restarts.

Same pattern for LobbyGameStore.


Key Files by Concern

Concern Key Files
HTTP routing apps/api/src/routes/apiRouter.ts, gameRouter.ts, lobbyRouter.ts
Controllers apps/api/src/controllers/gameController.ts, lobbyController.ts
Services apps/api/src/services/gameService.ts, multiplayerGameService.ts, lobbyService.ts
Models packages/db/src/models/termModel.ts, lobbyModel.ts
WS handlers apps/api/src/ws/handlers/gameHandlers.ts, lobbyHandlers.ts
WS router apps/api/src/ws/router.ts
WS auth apps/api/src/ws/auth.ts
Shared schemas packages/shared/src/schemas/game.ts, lobby.ts, auth.ts
Constants packages/shared/src/constants.ts
DB schema packages/db/src/db/schema.ts
Auth config apps/api/src/lib/auth.ts
Auth middleware apps/api/src/middleware/authMiddleware.ts