- Configure PostgreSQL 18 and Valkey 9.1 services - Create multi-stage Dockerfiles for API and Web apps - Set up pnpm workspace support in container builds - Configure hot reload via volume mounts for both services - Add healthchecks for service orchestration - Support dev/production stage targets (tsx watch vs compiled)
7.3 KiB
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.
- 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 — just validates the pipeline works)
docker-compose.ymlfor local dev:api,web,postgres,valkey.env.examplefiles forapps/apiandapps/web- 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/terms?pair=en-it&limit=10 returns 10 terms, each with 3 distractors attached.
- Run
scripts/extract_omw.pylocally → generatespackages/db/src/seed.json - Write Drizzle schema:
terms,translations,language_pairs - Write and run migration
- Write
packages/db/src/seed.ts(readsseed.json, populates tables) - Implement
TermRepository.getRandom(pairId, limit) - Implement
QuizService.attachDistractors(terms)— same POS, server-side, no duplicates - Implement
GET /language-pairsandGET /termsendpoints - Define Zod response schemas in
packages/shared - 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 - 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 "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.tsattaches 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:
/singleplayerroute useQuizSessionhook: fetch terms, manage question index + score stateQuestionCardcomponent: prompt word + 4 answer buttonsOptionButtoncomponent: idle / correct / wrong statesScoreScreencomponent: final score + play-again button- TanStack Query integration for
GET /terms - RTL tests for
QuestionCardandOptionButton - 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 - 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 the Express HTTP server - WS auth middleware: validate OpenAuth JWT on upgrade
- WS message router: dispatch incoming messages by
type room:join/room:leavehandlers → broadcastroom:stateto all room members- Room membership tracked in Valkey (ephemeral) +
room_playersin 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.tssingleton WS client with reconnect on drop - Frontend: Zustand
gameStorehandles incomingroom:stateevents - 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 timerroom:startWS handler → begin question loop, 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, updaterooms.status+room_players.scorein DB - Frontend:
/multiplayer/game/:coderoute - Frontend: extend Zustand store with
currentQuestion,roundAnswers,scores - Frontend: reuse
QuestionCard+OptionButton; add countdown timer ring - Frontend:
ScoreBoardcomponent — live per-player scores after each round - Frontend:
GameFinishedscreen — 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_HOSTenv vars - Production
.envfiles on VPS (OpenAuth secrets, DB credentials, Valkey URL) - Drizzle migration runs on
apicontainer start - Seed production DB (run
seed.tsonce) - 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/statsendpoint + 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)