156 lines
6.1 KiB
Markdown
156 lines
6.1 KiB
Markdown
# 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` (0–3) 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
|
||
|
||
```typescript
|
||
// 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` |
|