9.8 KiB
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
apiandweband 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.ymlfor local dev:api,web,postgres,valkey.env.examplefiles forapps/apiandapps/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.pylocally → 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
GameRequestSchemainpackages/shared - Define
AnswerOption,GameQuestion,GameSession,AnswerSubmission,AnswerResultschemas - 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 sessionevaluateAnswer()— looks up session, compares submitted optionId to stored correct answerGameSessionStoreinterface +InMemoryGameSessionStore(swappable to Valkey)
API endpoints
POST /api/v1/game/start— route, controller, servicePOST /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.
GameSetupcomponent (language, POS, difficulty, rounds)QuestionCardcomponent (prompt word + 4 answer buttons)OptionButtoncomponent (idle / correct / wrong states)ScoreScreencomponent (final score + play again)- Vite proxy configured for dev
selectedOptionIdadded toAnswerResult(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-authand configure with Drizzle adapter + PostgreSQL - Mount Better Auth handler on
/api/auth/*inapp.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/reactclient - Frontend: login page with Google + GitHub buttons
- Frontend: TanStack Router auth guard using
useSession - Frontend: TanStack Query
api.tssends 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.compointing 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.ymlwith 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
.envwith 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/configconfigured - 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.mdcovering 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 /roomsandPOST /rooms/:code/joinREST endpointsRoomService: create room with short code, join room, enforce max player limit- WebSocket server: attach
wsupgrade handler to Express HTTP server - WS auth middleware: validate JWT on upgrade
- WS message router: dispatch by
type room:join/room:leavehandlers → broadcastroom: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: 2–4 players complete a 10-round game with correct live scores and a winner screen.
GameService: generate question sequence, enforce 15s server timerroom:startWS handler → broadcast firstgame:questiongame:answerWS 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/:coderoute - Frontend: reuse
QuestionCard+OptionButton; add countdown timer - Frontend:
ScoreBoardcomponent — live per-player scores - Frontend:
GameFinishedscreen — 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/statsendpoint + 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)