lila/documentation/roadmap.md
lila 3f7bc4111e chore: rename project from glossa to lila
- Update all package names from @glossa/* to @lila/*
- Update all imports, container names, volume names
- Update documentation references
- Recreate database with new credentials
2026-04-13 10:00:52 +02:00

8 KiB
Raw Blame History

lila — Roadmap

Each phase produces a working increment. Nothing is built speculatively.


Phase 0 — Foundation

Goal: Empty repo that builds, lints, and runs end-to-end. Done when: pnpm dev starts both apps; GET /api/health returns 200; React renders a hello page.

  • Initialise pnpm workspace monorepo: apps/web, apps/api, packages/shared, packages/db
  • Configure TypeScript project references across packages
  • Set up ESLint + Prettier with shared configs in root
  • Set up Vitest in api and web and both packages
  • Scaffold Express app with GET /api/health
  • Scaffold Vite + React app with TanStack Router (single root route)
  • Configure Drizzle ORM + connection to local PostgreSQL
  • Write first migration (empty — validates the pipeline works)
  • docker-compose.yml for local dev: api, web, postgres, valkey
  • .env.example files for apps/api and apps/web

Phase 1 — Vocabulary Data + API

Goal: Word data lives in the DB and can be queried via the API. Done when: API returns quiz sessions with distractors, error handling and tests in place.

Data pipeline

  • Run extract-en-it-nouns.py locally → generates JSON
  • Write Drizzle schema: terms, translations, term_glosses, decks, deck_terms
  • Write and run migration (includes CHECK constraints)
  • Write packages/db/src/seeding-datafiles.ts (imports all terms + translations)
  • Write packages/db/src/generating-deck.ts (idempotent deck generation)
  • CEFR enrichment pipeline complete for English and Italian
  • Expand data pipeline — import all OMW languages and POS

Schemas

  • Define GameRequestSchema in packages/shared
  • Define AnswerOption, GameQuestion, GameSession, AnswerSubmission, AnswerResult schemas
  • Derived types exported from constants (SupportedLanguageCode, SupportedPos, DifficultyLevel)

Model layer

  • getGameTerms() with POS / language / difficulty / limit filters
  • Double join on translations (source + target language)
  • Gloss left join
  • getDistractors() with POS / difficulty / language / excludeTermId / excludeText filters
  • Models correctly placed in packages/db

Service layer

  • createGameSession() — fetches terms, fetches distractors, shuffles options, stores session
  • evaluateAnswer() — looks up session, compares submitted optionId to stored correct answer
  • GameSessionStore interface + InMemoryGameSessionStore (swappable to Valkey)

API endpoints

  • POST /api/v1/game/start — route, controller, service
  • POST /api/v1/game/answer — route, controller, service
  • End-to-end pipeline verified with test script

Error handling

  • Typed error classes: AppError, ValidationError (400), NotFoundError (404)
  • Central error middleware in app.ts
  • Controllers cleaned up: validate → call service → next(error) on failure

Tests

  • Unit tests for createGameSession (question shape, options, distractors, gloss)
  • Unit tests for evaluateAnswer (correct, incorrect, missing session, missing question)
  • Integration tests for both endpoints via supertest (200, 400, 404)

Phase 2 — Singleplayer Quiz UI

Goal: A user can complete a full quiz in the browser. Done when: User visits /play, configures settings, answers questions, sees score screen, can play again.

  • GameSetup component (language, POS, difficulty, rounds)
  • QuestionCard component (prompt word + 4 answer buttons)
  • OptionButton component (idle / correct / wrong states)
  • ScoreScreen component (final score + play again)
  • Vite proxy configured for dev
  • selectedOptionId added to AnswerResult (discovered during frontend work)

Phase 3 — Auth

Goal: Users can log in via Google or GitHub and stay logged in. Done when: Better Auth session is validated on protected routes; unauthenticated users are redirected to login; user row is created on first social login.

  • Install better-auth and configure with Drizzle adapter + PostgreSQL
  • Mount Better Auth handler on /api/auth/* in app.ts
  • Configure Google and GitHub social providers
  • Run Better Auth CLI to generate and migrate auth tables (user, session, account, verification)
  • Add session validation middleware for protected API routes
  • Frontend: install better-auth/react client
  • Frontend: login page with Google + GitHub buttons
  • Frontend: TanStack Router auth guard using useSession
  • Frontend: TanStack Query api.ts sends credentials with every request
  • Unit tests for session middleware

Phase 4 — Multiplayer Lobby

Goal: Players can create and join rooms; the host sees all joined players in real time. Done when: Two browser tabs can join the same room and see each other's display names update live via WebSocket.

  • Write Drizzle schema: rooms, room_players
  • Write and run migration
  • POST /rooms and POST /rooms/:code/join REST endpoints
  • RoomService: create room with short code, join room, enforce max player limit
  • WebSocket server: attach ws upgrade handler to Express HTTP server
  • WS auth middleware: validate JWT on upgrade
  • WS message router: dispatch by type
  • room:join / room:leave handlers → broadcast room:state
  • Room membership tracked in Valkey (ephemeral) + PostgreSQL (durable)
  • Define all WS event Zod schemas in packages/shared
  • Frontend: /multiplayer/lobby — create room + join-by-code
  • Frontend: /multiplayer/room/:code — player list, room code, "Start Game" (host only)
  • Frontend: WS client singleton with reconnect

Phase 5 — Multiplayer Game

Goal: Host starts a game; all players answer simultaneously in real time; a winner is declared. Done when: 24 players complete a 10-round game with correct live scores and a winner screen.

  • GameService: generate question sequence, enforce 15s server timer
  • room:start WS handler → broadcast first game:question
  • game:answer WS handler → collect per-player answers
  • On all-answered or timeout → evaluate, broadcast game:answer_result
  • After N rounds → broadcast game:finished, update DB (transactional)
  • Frontend: /multiplayer/game/:code route
  • Frontend: reuse QuestionCard + OptionButton; add countdown timer
  • Frontend: ScoreBoard component — live per-player scores
  • Frontend: GameFinished screen — winner highlight, final scores, play again
  • Unit tests for GameService (round evaluation, tie-breaking, timeout)

Phase 6 — Production Deployment

Goal: App is live on Hetzner, accessible via HTTPS on all subdomains. Done when: https://app.yourdomain.com loads; wss://api.yourdomain.com connects; auth flow works end-to-end.

  • docker-compose.prod.yml: all services + nginx-proxy + acme-companion
  • Nginx config per container: VIRTUAL_HOST + LETSENCRYPT_HOST
  • Production .env files on VPS
  • Drizzle migration runs on api container start
  • Seed production DB
  • Smoke test: login → solo game → multiplayer game end-to-end

Phase 7 — Polish & Hardening

Goal: Production-ready for real users.

  • Rate limiting on API endpoints
  • Graceful WS reconnect with exponential back-off
  • React error boundaries
  • GET /users/me/stats endpoint + profile page
  • Accessibility pass (keyboard nav, ARIA on quiz buttons)
  • Favicon, page titles, Open Graph meta
  • CI/CD pipeline (GitHub Actions → SSH deploy)
  • Database backups (cron → Hetzner Object Storage)

Dependency Graph

Phase 0 (Foundation) ✅
└── Phase 1 (Vocabulary Data + API) ✅
    └── Phase 2 (Singleplayer UI) ✅
        └── Phase 3 (Auth)
            ├── Phase 4 (Multiplayer Lobby)
            │   └── Phase 5 (Multiplayer Game)
            │       └── Phase 6 (Deployment)
            └── Phase 7 (Hardening)