8 KiB
Glossa — 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: JWT from OpenAuth is validated by the API; protected routes redirect unauthenticated users; user row is created on first login.
- Add OpenAuth service to
docker-compose.yml - Write Drizzle schema:
users(uuidid, textopenauth_sub) - Write and run migration
- Implement JWT validation middleware in
apps/api - Implement
GET /api/auth/me(validate token, upsert user row, return user) - Define auth Zod schemas in
packages/shared - Frontend: login page with Google + GitHub buttons
- Frontend: redirect to auth service → receive JWT → store in memory + HttpOnly cookie
- Frontend: TanStack Router auth guard
- Frontend: TanStack Query
api.tsattaches token to every request - Unit tests for JWT 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 /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 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
.envfiles on VPS - Drizzle migration runs on
apicontainer 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/statsendpoint + 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)