lila/documentation/roadmap.md
lila e5595b5039
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s
updating documentation
2026-04-14 19:35:49 +02:00

9.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 6 — Production Deployment

Goal: App is live on Hetzner, accessible via HTTPS on all subdomains. Done when: https://lilastudy.com loads; https://api.lilastudy.com responds; auth flow works end-to-end; CI/CD deploys on push to main.

Note: Deployment was moved ahead of multiplayer — the app is useful without multiplayer but not without deployment.

Infrastructure

  • Hetzner VPS provisioned (Debian 13, ARM64, 4GB RAM)
  • SSH hardening, ufw firewall, fail2ban
  • Docker + Docker Compose installed
  • Domain DNS: A record + wildcard *.lilastudy.com pointing to VPS

Reverse proxy

  • Caddy container with automatic HTTPS (Let's Encrypt)
  • Subdomain routing: lilastudy.com → web, api.lilastudy.com → API, git.lilastudy.com → Forgejo

Docker stack

  • Production docker-compose.yml with all services on shared network
  • No ports exposed on internal services — only Caddy (80/443) and Forgejo SSH (2222)
  • Production Dockerfile stages for API (runner) and frontend (nginx:alpine)
  • Monorepo package exports fixed for production (dist/src paths)
  • Production .env with env-driven CORS, auth URLs, cookie domain

Git server + container registry

  • Forgejo running with built-in container registry
  • SSH on port 2222, dev laptop ~/.ssh/config configured
  • Repository created, code pushed

CI/CD

  • Forgejo Actions enabled
  • Forgejo Runner container on VPS with Docker socket access
  • .forgejo/workflows/deploy.yml — build, push, deploy via SSH on push to main
  • Registry and SSH secrets configured in Forgejo

Database

  • Initial seed via pg_dump from dev laptop
  • Seeding script is idempotent (onConflictDoNothing) for future data additions
  • Schema migrations via Drizzle (migrate first, deploy second)

OAuth

  • Google and GitHub OAuth redirect URIs configured for production
  • Cross-subdomain cookies via COOKIE_DOMAIN=.lilastudy.com

Backups

  • Daily cron job (3 AM) with pg_dump, 7-day retention
  • Dev laptop auto-syncs backups on login via rsync

Documentation

  • deployment.md covering full infrastructure setup

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 7 — Polish & Hardening

Goal: Production-ready for real users.

  • CI/CD pipeline (Forgejo Actions → SSH deploy)
  • Database backups (cron → dev laptop sync)
  • 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
  • Offsite backup storage (Hetzner Object Storage)
  • Monitoring/logging (uptime, centralized logs)
  • Valkey for game session store (replace in-memory)

Dependency Graph

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