# Vocabulary Trainer — Roadmap Each phase produces a working, deployable 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. [x] Initialise pnpm workspace monorepo: `apps/web`, `apps/api`, `packages/shared`, `packages/db` [x] Configure TypeScript project references across packages [x] Set up ESLint + Prettier with shared configs in root [x] Set up Vitest in `api` and `web` and both packages [x] Scaffold Express app with `GET /api/health` [x] Scaffold Vite + React app with TanStack Router (single root route) [x] Configure Drizzle ORM + connection to local PostgreSQL [x] Write first migration (empty — just validates the pipeline works) [x] `docker-compose.yml` for local dev: `api`, `web`, `postgres`, `valkey` [x] `.env.example` files for `apps/api` and `apps/web` [x] update decisions.md ## Phase 1 — Vocabulary Data Goal: Word data lives in the DB and can be queried via the API. Done when: `GET /api/decks/1/terms?limit=10` returns 10 terms from a specific deck. [x] Run `extract-en-it-nouns.py` locally → generates `datafiles/en-it-nouns.json` [x] Write Drizzle schema: `terms`, `translations`, `language_pairs`, `term_glosses`, `decks`, `deck_terms` [x] Write and run migration (includes CHECK constraints for `pos`, `gloss_type`) [x] Write `packages/db/src/seed.ts` (imports ALL terms + translations, NO decks) [x] Download CEFR A1/A2 noun lists (from GitHub repos) [x] Write `scripts/build_decks.ts` (reads external CEFR lists, matches to DB, creates decks) [x] Run `pnpm db:seed` → populates terms [x] Run `pnpm db:build-deck` → creates curated decks [ ] Define Zod response schemas in `packages/shared` [ ] Implement `DeckRepository.getTerms(deckId, limit, offset)` [ ] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates [ ] Implement `GET /language-pairs`, `GET /decks`, `GET /decks/:id/terms` endpoints [ ] Unit tests for `QuizService` (correct POS filtering, never includes the answer) [ ] update decisions.md ## Phase 2 — 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` (uuid `id`, text `openauth_sub`, no games_played/won columns) [ ] Write and run migration (includes `updated_at` + triggers) [ ] Implement JWT validation middleware in `apps/api` [ ] Implement `GET /api/auth/me` (validate token, upsert user row via `openauth_sub`, return user) [ ] Define auth Zod schemas in `packages/shared` [ ] Frontend: login page with "Continue with Google" + "Continue with GitHub" buttons [ ] Frontend: redirect to `auth.yourdomain.com` → receive JWT → store in memory + HttpOnly cookie [ ] Frontend: TanStack Router auth guard (redirects unauthenticated users) [ ] Frontend: TanStack Query `api.ts` attaches token to every request [ ] Unit tests for JWT middleware [ ] update decisions.md ## Phase 3 — Single-player Mode Goal: A logged-in user can complete a full solo quiz session. Done when: User sees 10 questions, picks answers, sees their final score. [ ] Frontend: `/singleplayer` route [ ] `useQuizSession` hook: fetch terms, manage question index + score state [ ] `QuestionCard` component: prompt word + 4 answer buttons [ ] `OptionButton` component: idle / correct / wrong states [ ] `ScoreScreen` component: final score + play-again button [ ] TanStack Query integration for `GET /terms` [ ] RTL tests for `QuestionCard` and `OptionButton` [ ] update decisions.md ## Phase 4 — Multiplayer Rooms (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` (add `deck_id` FK to rooms) [ ] Write and run migration (includes CHECK constraints: `code=UPPER(code)`, `status`, `max_players`) [ ] Add indexes: `idx_rooms_host`, `idx_room_players_score` [ ] `POST /rooms` and `POST /rooms/:code/join` REST endpoints [ ] `RoomService`: create room with short code, join room, enforce max player limit [ ] `POST /rooms` accepts `deck_id` (which vocabulary deck to use) [ ] WebSocket server: attach `ws` upgrade handler to the Express HTTP server [ ] WS auth middleware: validate OpenAuth JWT on upgrade [ ] WS message router: dispatch incoming messages by `type` [ ] `room:join` / `room:leave` handlers → broadcast `room:state` to all room members [ ] Room membership tracked in Valkey (ephemeral) + `room_players` in PostgreSQL (durable) [ ] Define all WS event Zod schemas in `packages/shared` [ ] Frontend: `/multiplayer/lobby` — create room form + join-by-code form [ ] Frontend: `/multiplayer/room/:code` — player list, room code display, "Start Game" (host only) [ ] Frontend: `ws.ts` singleton WS client with reconnect on drop [ ] Frontend: Zustand `gameStore` handles incoming `room:state` events [ ] update decisions.md ## 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 for a room, enforce server-side 15 s timer [ ] `room:start` WS handler → begin question loop, 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 `rooms.status` + `room_players.score` in DB (transactional) [ ] Frontend: `/multiplayer/game/:code` route [ ] Frontend: extend Zustand store with `currentQuestion`, `roundAnswers`, `scores` [ ] Frontend: reuse `QuestionCard` + `OptionButton`; add countdown timer ring [ ] Frontend: `ScoreBoard` component — live per-player scores after each round [ ] Frontend: `GameFinished` screen — winner highlight, final scores, "Play Again" button [ ] Unit tests for `GameService` (round evaluation, tie-breaking, timeout auto-advance) [ ] update decisions.md ## 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` env vars [ ] Production `.env` files on VPS (OpenAuth secrets, DB credentials, Valkey URL) [ ] Drizzle migration runs on `api` container start (includes CHECK constraints + triggers) [ ] Seed production DB (run `seed.ts` once) [ ] Smoke test: login → solo game → create room → multiplayer game end-to-end [ ] update decisions.md ## Phase 7 — Polish & Hardening (post-MVP) Not required to ship, but address before real users arrive. [ ] Rate limiting on API endpoints (`express-rate-limit`) [ ] Graceful WS reconnect with exponential back-off [ ] React error boundaries [ ] `GET /users/me/stats` endpoint (aggregates from `room_players`) + profile page [ ] Accessibility pass (keyboard nav, ARIA on quiz buttons) [ ] Favicon, page titles, Open Graph meta [ ] CI/CD pipeline (GitHub Actions → SSH deploy on push to `main`) [ ] Database backups (cron → Hetzner Object Storage) [ ] update decisions.md Dependency Graph Phase 0 (Foundation) └── Phase 1 (Vocabulary Data) └── Phase 2 (Auth) ├── Phase 3 (Singleplayer) ← parallel with Phase 4 └── Phase 4 (Room Lobby) └── Phase 5 (Multiplayer Game) └── Phase 6 (Deployment) --- ## ui sketch i was sketching the ui of the menu and came up with some questions.  this would be the flow to start a single player game: main menu => singleplayer, multiplayer, settings singleplayer => language selection "i speak english" => "i want to learn italian" (both languages are dropdowns to select the fitting language) language selection => category selection => pure grammar, media (as disussed, practicing on song lyrics or breaking bad subtitles) pure grammar => pos selection => nouns or verbs (in mvp) nouns has 3 subcategories => singular (1-on-1 translation dog => cane), plural (plural practices cane => cani for example), gender/articles (il cane or la cane for example) verbs has 2 subcategories => infinitv (1-on-1 translation to talk => parlare) or conjugations (user gets shown the infinitiv and a table with all personal pronouns and has to fill in the gaps with the according conjugations) pos selection => difficulty selection (from a1 to c2) afterwards start game button --- this begs the questions: - how to store the plural, articles of nouns in database - how to store the conjugations of verbs - what about ipa? - links to audiofiles to listen how a word is pronounced? - one table for italian_verbs, french_nouns, german_adjectives?