updating documentation

This commit is contained in:
lila 2026-05-16 01:59:43 +02:00
parent 1ba57c7e9d
commit 7e0311683f
25 changed files with 2660 additions and 226 deletions

View file

@ -0,0 +1,118 @@
# notes
## prompt
ive attached the readme of my project. this is my current task:
task description.
1. tell me which files you need to see to get the full context of the problem
2. walk me text-only through the problem and the solution
3. if we need to update multiple files: lets go through them one by one, no matter how many files
4. if we go through a file, we'll do it slowly section by section, no matter how many sections
5. how to name the current feature branch? also tell me when its time to git commit and provide a commit message
6. if we have multiple options to do something, also always provide options that reflect current industry standards and best practices
7. never assume anything! always ask for clarification!
8. For every completed task, produce a ticket file in documentation/tickets/. Use ADR format (adr-) for decisions between options with long-term consequences. Use feat-/fix-/chore- for routine tasks. Always include a setup guide or summary of what was done. Suggest the filename.
## tasks
- **IMPORTANT** db migrations have to be part of the deployment pipeline!!!!!!!!!!!!!!!!!!
- put users in separate db
- pinning dependencies in package.json files
- rethink organisation of datafiles and wordlists
- admin dashboard for user management, also overview of words and languages and all their stats
## problems+thoughts
### IMPORTANT
verify if hetzner domain needs to be pushed, theres a change on hetzner and some domains need to be migrated
### redirect or page not found
subdomains or pages that dont exist should have page not found or should redirect
### docker credential helper
WARNING! Your credentials are stored unencrypted in '/home/languagedev/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
### docker containers on startup?
laptop: verify if docker containers run on startup (they shouldnt)
### vps setup
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
<<<<<<< HEAD
- ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
### ~~cd/ci pipeline~~ ✅ RESOLVED
Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `deployment.md`.
### ~~postgres backups~~ ✅ RESOLVED
# Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
> > > > > > > dev
### try now option
there should be an option to try the app without an account so users can see what they would get when creating an account
### resolve deps problem
﬌ pnpm --filter web add better-auth
WARN 2 deprecated subdependencies found: @esbuild-kit/core-utils@3.3.2, @esbuild-kit/esm-loader@2.6.5
Progress: resolved 577, reused 0, downloaded 0, added 0, done
WARN Issues with peer dependencies found
.
└─┬ eslint-plugin-react-hooks 7.0.1
└── ✕ unmet peer eslint@"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0": found 10.0.3
. | +3 +
Done in 5.6s using pnpm v10.33.0
### env managing
using env files is not uptodate, use a modern, proper approach
### apple login
add option to login with apple accounts
### mail login
add option to login with email+pw
### google login
credentials are saved in Downloads/lila/ json file
app publication/verification:
Branding and Data Access (Scope) Verification
In addition to brand verification, your app may also need to be verified to use certain scopes. You can view and track this on the Verification Center page:
Branding status: This tracks the verification of your app's public-facing brand (name, logo, etc.).
Data access status: This tracks the verification of the specific data (scopes) your app is requesting to access.
Note: You must have a published branding status before you can request verification for data access (scopes).
Manage App Audience Configuration
Publishing Status
Manage your app publishing status in the Audience page of the Google Auth Platform.
User Type
Manage your app audience in the Audience page of the Google Auth Platform.
[link](https://support.google.com/cloud/answer/15549049?visit_id=01775982668127-2568683599515917262&rd=1#publishing-status&zippy=%2Cpublishing-status%2Cuser-type)
## tipps
- backend advice: [backend](https://github.com/MohdOwaisShah/backend)
- openapi
- bruno for api testing
- tailscale
- musicforprogramming.net

View file

@ -0,0 +1,241 @@
# 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.
- [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 — validates the pipeline works)
- [x] `docker-compose.yml` for local dev: `api`, `web`, `postgres`, `valkey`
- [x] Root `.env.example` for local dev (`docker-compose.yml` + API)
---
## 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
- [x] Run `extract-en-it-nouns.py` locally → generates JSON
- [x] Write Drizzle schema: `terms`, `translations`, `term_glosses`, `decks`, `deck_terms`
- [x] Write and run migration (includes CHECK constraints)
- [x] Write `packages/db/src/seeding-datafiles.ts` (imports all terms + translations)
- [x] Write `packages/db/src/generating-deck.ts` (idempotent deck generation)
- [x] CEFR enrichment pipeline complete for English and Italian
- [x] Expand data pipeline — import all OMW languages and POS
### Schemas
- [x] Define `GameRequestSchema` in `packages/shared`
- [x] Define `AnswerOption`, `GameQuestion`, `GameSession`, `AnswerSubmission`, `AnswerResult` schemas
- [x] Derived types exported from constants (`SupportedLanguageCode`, `SupportedPos`, `DifficultyLevel`)
### Model layer
- [x] `getGameTerms()` with POS / language / difficulty / limit filters
- [x] Double join on `translations` (source + target language)
- [x] Gloss left join
- [x] `getDistractors()` with POS / difficulty / language / excludeTermId / excludeText filters
- [x] Models correctly placed in `packages/db`
### Service layer
- [x] `createGameSession()` — fetches terms, fetches distractors, shuffles options, stores session
- [x] `evaluateAnswer()` — looks up session, compares submitted optionId to stored correct answer
- [x] `GameSessionStore` interface + `InMemoryGameSessionStore` (swappable to Valkey)
### API endpoints
- [x] `POST /api/v1/game/start` — route, controller, service
- [x] `POST /api/v1/game/answer` — route, controller, service
- [x] End-to-end pipeline verified with test script
### Error handling
- [x] Typed error classes: `AppError`, `ValidationError` (400), `NotFoundError` (404)
- [x] Central error middleware in `app.ts`
- [x] Controllers cleaned up: validate → call service → `next(error)` on failure
### Tests
- [x] Unit tests for `createGameSession` (question shape, options, distractors, gloss)
- [x] Unit tests for `evaluateAnswer` (correct, incorrect, missing session, missing question)
- [x] 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.
- [x] `GameSetup` component (language, POS, difficulty, rounds)
- [x] `QuestionCard` component (prompt word + 4 answer buttons)
- [x] `OptionButton` component (idle / correct / wrong states)
- [x] `ScoreScreen` component (final score + play again)
- [x] Vite proxy configured for dev
- [x] `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.
- [x] Install `better-auth` and configure with Drizzle adapter + PostgreSQL
- [x] Mount Better Auth handler on `/api/auth/*` in `app.ts`
- [x] Configure Google and GitHub social providers
- [x] Run Better Auth CLI to generate and migrate auth tables (user, session, account, verification)
- [x] Add session validation middleware for protected API routes
- [x] Frontend: install `better-auth/react` client
- [x] Frontend: login page with Google + GitHub buttons
- [x] Frontend: TanStack Router auth guard using `useSession`
- [x] Frontend: TanStack Query `api.ts` sends credentials with every request
- [x] 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
- [x] Hetzner VPS provisioned (Debian 13, ARM64, 4GB RAM)
- [x] SSH hardening, ufw firewall, fail2ban
- [x] Docker + Docker Compose installed
- [x] Domain DNS: A record + wildcard `*.lilastudy.com` pointing to VPS
### Reverse proxy
- [x] Caddy container with automatic HTTPS (Let's Encrypt)
- [x] Subdomain routing: `lilastudy.com` → web, `api.lilastudy.com` → API, `git.lilastudy.com` → Forgejo
### Docker stack
- [x] Production `docker-compose.yml` with all services on shared network
- [x] No ports exposed on internal services — only Caddy (80/443) and Forgejo SSH (2222)
- [x] Production Dockerfile stages for API (runner) and frontend (nginx:alpine)
- [x] Monorepo package exports fixed for production (dist/src paths)
- [x] Production `.env` with env-driven CORS, auth URLs, cookie domain
### Git server + container registry
- [x] Forgejo running with built-in container registry
- [x] SSH on port 2222, dev laptop `~/.ssh/config` configured
- [x] Repository created, code pushed
### CI/CD
- [x] Forgejo Actions enabled
- [x] Forgejo Runner container on VPS with Docker socket access
- [x] `.forgejo/workflows/deploy.yml` — build, push, deploy via SSH on push to main
- [x] Registry and SSH secrets configured in Forgejo
### Database
- [x] Initial seed via pg_dump from dev laptop
- [x] Seeding script is idempotent (onConflictDoNothing) for future data additions
- [x] Schema migrations via Drizzle (migrate first, deploy second)
### OAuth
- [x] Google and GitHub OAuth redirect URIs configured for production
- [x] Cross-subdomain cookies via COOKIE_DOMAIN=.lilastudy.com
### Backups
- [x] Daily cron job (3 AM) with pg_dump, 7-day retention
- [x] Dev laptop auto-syncs backups on login via rsync
### Documentation
- [x] `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.
- [x] Write Drizzle schema: `lobbies`, `lobby_players`
- [x] Write and run migration
- [x] `POST /api/v1/lobbies` and `POST /api/v1/lobbies/:code/join` REST endpoints
- [x] `LobbyService`: create lobby with Crockford Base32 code, join lobby, enforce max player limit
- [x] WebSocket server: attach `ws` upgrade handler to Express HTTP server
- [x] WS auth middleware: validate Better Auth session on upgrade
- [x] WS message router: dispatch by `type` via Zod discriminated union
- [x] `lobby:join` / `lobby:leave` handlers → broadcast `lobby:state`
- [x] Lobby membership tracked in PostgreSQL (durable), game state in-memory (Valkey deferred)
- [x] Define all WS event Zod schemas in `packages/shared`
- [x] Frontend: `/multiplayer` — create lobby + join-by-code
- [x] Frontend: `/multiplayer/lobby/:code` — player list, lobby code, "Start Game" (host only)
- [x] Frontend: WS client class with typed message handlers
---
## 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 3-round game with correct live scores and a winner screen.
- [x] `MultiplayerGameService`: generate question sequence, enforce 15s server timer
- [x] `lobby:start` WS handler → broadcast first `game:question`
- [x] `game:answer` WS handler → collect per-player answers
- [x] On all-answered or timeout → evaluate, broadcast `game:answer_result`
- [x] After N rounds → broadcast `game:finished`, update DB (transactional)
- [x] Frontend: `/multiplayer/game/:code` route
- [x] Frontend: reuse `QuestionCard` + `OptionButton`; round results per player
- [x] Frontend: `MultiplayerScoreScreen` — winner highlight, final scores, play again
- [x] Unit tests for `LobbyService`, WS auth, WS router
---
## Phase 7 — Polish & Hardening
**Goal:** Production-ready for real users.
- [x] CI/CD pipeline (Forgejo Actions → SSH deploy)
- [x] 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
```text
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)
```

View file

@ -0,0 +1,394 @@
# lila — Project Specification
> **This document is the single source of truth for the project.**
> It is written to be handed to any LLM as context. It contains the project vision, the current MVP scope, the tech stack, the architecture, and the roadmap.
---
## 1. Project Overview
A vocabulary trainer for EnglishItalian words. The quiz format is Duolingo-style: one word is shown as a prompt, and the user picks the correct translation from four choices (1 correct + 3 distractors of the same part-of-speech). The app supports both singleplayer and real-time multiplayer game modes.
**The core learning loop:**
Show word → pick answer → see result → next word → final score
The vocabulary data comes from WordNet + the Open Multilingual Wordnet (OMW). A one-time Python script extracts EnglishItalian noun pairs and seeds the database. The data model is language-pair agnostic by design — adding a new language later requires no schema changes.
### Core Principles
- **Minimal but extendable**: working product fast, clean architecture for future growth
- **Mobile-first**: touch-friendly Duolingo-like UX
- **Type safety end-to-end**: TypeScript + Zod schemas shared between frontend and backend
---
## 2. Full Product Vision (Long-Term)
- Users log in via Google or GitHub (Better Auth)
- Singleplayer mode: 10-round quiz, score screen
- Multiplayer mode: create a room, share a code, 24 players answer simultaneously in real time, live scores, winner screen
- 1000+ EnglishItalian nouns seeded from WordNet
This is the full vision. The current implementation already covers most of it; remaining items are captured in the roadmap and the Post-MVP ladder below.
---
## 3. MVP Scope
**Goal:** A working, presentable vocabulary trainer that can be shown to real people (singleplayer and multiplayer), with a production deployment.
### What is IN the MVP
- Vocabulary data in a PostgreSQL database (already seeded)
- REST API that returns quiz terms with distractors
- Singleplayer quiz UI: configurable rounds (3 or 10), answer feedback, score screen
- Clean, mobile-friendly UI (Tailwind + shadcn/ui)
- Global error handler with typed error classes
- Unit + integration tests for the API
- Authentication via Better Auth (Google + GitHub)
- Multiplayer lobby + game over WebSockets
- Production deployment (Docker Compose + Caddy + Hetzner) and CI/CD (Forgejo Actions)
### What is CUT from the MVP
| Feature | Why cut |
| --------------------- | ---------- |
| User stats / profiles | Needs auth |
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
---
## 4. Technology Stack
The monorepo structure and tooling are already set up. This is the full stack.
| Layer | Technology | Status |
| ------------ | ------------------------------ | ------------------------------------------------------ |
| Monorepo | pnpm workspaces | ✅ |
| Frontend | React 18, Vite, TypeScript | ✅ |
| Routing | TanStack Router | ✅ |
| Server state | TanStack Query | ✅ |
| Client state | Zustand | ✅ |
| Styling | Tailwind CSS + shadcn/ui | ✅ |
| Backend | Node.js, Express, TypeScript | ✅ |
| Database | PostgreSQL + Drizzle ORM | ✅ |
| Validation | Zod (shared schemas) | ✅ |
| Testing | Vitest, supertest | ✅ |
| Auth | Better Auth (Google + GitHub) | ✅ |
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
| CI/CD | Forgejo Actions | ✅ |
| Realtime | WebSockets (`ws` library) | ✅ |
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) |
---
## 5. Repository Structure
```text
lila/
├── .forgejo/
│ └── workflows/
│ └── deploy.yml — CI/CD pipeline (build, push, deploy)
├── apps/
│ ├── api/
│ │ └── src/
│ │ ├── app.ts — createApp() factory, CORS, auth handler, error middleware
│ │ ├── server.ts — starts server on PORT
│ │ ├── errors/
│ │ │ └── AppError.ts — AppError, ValidationError, NotFoundError
│ │ ├── lib/
│ │ │ └── auth.ts — Better Auth config (Google + GitHub providers)
│ │ ├── middleware/
│ │ │ ├── authMiddleware.ts — session validation for protected routes
│ │ │ └── errorHandler.ts — central error middleware
│ │ ├── routes/
│ │ │ ├── apiRouter.ts — mounts /health and /game routers
│ │ │ ├── gameRouter.ts — POST /start, POST /answer
│ │ │ └── healthRouter.ts
│ │ ├── controllers/
│ │ │ └── gameController.ts — validates input, calls service, sends response
│ │ ├── services/
│ │ │ ├── gameService.ts — builds quiz sessions, evaluates answers
│ │ │ └── gameService.test.ts — unit tests (mocked DB)
│ │ └── gameSessionStore/
│ │ ├── GameSessionStore.ts — interface (async, Valkey-ready)
│ │ ├── InMemoryGameSessionStore.ts
│ │ └── index.ts
│ └── web/
│ ├── Dockerfile — multi-stage: dev + production (nginx:alpine)
│ ├── nginx.conf — SPA fallback routing
│ └── src/
│ ├── routes/
│ │ ├── index.tsx — landing page
│ │ ├── play.tsx — the quiz
│ │ ├── login.tsx — Google + GitHub login buttons
│ │ ├── about.tsx
│ │ └── __root.tsx
│ ├── lib/
│ │ └── auth-client.ts — Better Auth React client
│ ├── components/
│ │ └── game/
│ │ ├── GameSetup.tsx — settings UI
│ │ ├── QuestionCard.tsx — prompt + 4 options
│ │ ├── OptionButton.tsx — idle / correct / wrong states
│ │ └── ScoreScreen.tsx — final score + play again
│ └── main.tsx
├── packages/
│ ├── shared/
│ │ └── src/
│ │ ├── constants.ts — SUPPORTED_POS, DIFFICULTY_LEVELS, etc.
│ │ ├── schemas/game.ts — Zod schemas for all game types
│ │ └── index.ts
│ └── db/
│ ├── drizzle/ — migration SQL files
│ └── src/
│ ├── db/schema.ts — Drizzle schema (terms, translations, auth tables)
│ ├── models/termModel.ts — getGameTerms(), getDistractors()
│ ├── seeding-datafiles.ts — seeds terms + translations from JSON
│ ├── seeding-cefr-levels.ts — enriches translations with CEFR data
│ ├── generating-deck.ts — builds curated decks
│ └── index.ts
├── scripts/ — Python extraction/comparison/merge scripts
├── documentation/ — project docs
├── docker-compose.yml — local dev stack
├── Caddyfile — reverse proxy routing
└── pnpm-workspace.yaml
```
`packages/shared` is the contract between frontend and backend. All request/response shapes are defined there as Zod schemas — never duplicated.
---
## 6. Architecture
### The Layered Architecture
```text
HTTP Request
Router — maps URL + HTTP method to a controller
Controller — handles HTTP only: validates input, calls service, sends response
Service — business logic only: no HTTP, no direct DB access
Model — database queries only: no business logic
Database
```
**The rule:** each layer only talks to the layer directly below it. A controller never touches the database. A service never reads `req.body`. A model never knows what a quiz is.
### Monorepo Package Responsibilities
| Package | Owns |
| ----------------- | -------------------------------------------------------- |
| `packages/shared` | Zod schemas, constants, derived TypeScript types |
| `packages/db` | Drizzle schema, DB connection, all model/query functions |
| `apps/api` | Router, controllers, services, error handling |
| `apps/web` | React frontend, consumes types from shared |
**Key principle:** all database code lives in `packages/db`. `apps/api` never imports `drizzle-orm` for queries — it only calls functions exported from `packages/db`.
### Production Infrastructure
```text
Internet → Caddy (HTTPS termination)
├── lilastudy.com → web container (nginx, static files)
├── api.lilastudy.com → api container (Express, port 3000)
└── git.lilastudy.com → forgejo container (git + registry, port 3000)
SSH (port 2222) → forgejo container (git push/pull)
```
All containers communicate over an internal Docker network. Only Caddy (80/443) and Forgejo SSH (2222) are exposed to the internet.
---
## 7. Data Model (Current State)
Words are modelled as language-neutral concepts (terms) separate from learning curricula (decks). Adding a new language pair requires no schema changes — only new rows in `translations`, `decks`.
**Core tables:** `terms`, `translations`, `term_glosses`, `decks`, `deck_terms`, `topics`, `term_topics`
**Auth tables (managed by Better Auth):** `user`, `session`, `account`, `verification`
Key columns on `terms`: `id` (uuid), `pos` (CHECK-constrained), `source`, `source_id` (unique pair for idempotent imports)
Key columns on `translations`: `id`, `term_id` (FK), `language_code` (CHECK-constrained), `text`, `cefr_level` (nullable varchar(2), CHECK A1C2)
Deck model uses `source_language` + `validated_languages` array — one deck serves multiple target languages. Decks are frequency tiers (e.g. `en-core-1000`), not POS splits.
Full schema is in `packages/db/src/db/schema.ts`.
---
## 8. API
### Endpoints
```text
POST /api/v1/game/start GameRequest → GameSession (requires auth)
POST /api/v1/game/answer AnswerSubmission → AnswerResult (requires auth)
GET /api/v1/health Health check (public)
ALL /api/auth/* Better Auth handlers (public)
```
### Schemas (packages/shared)
**GameRequest:** `{ source_language, target_language, pos, difficulty, rounds }`
**GameSession:** `{ sessionId: uuid, questions: GameQuestion[] }`
**GameQuestion:** `{ questionId: uuid, prompt: string, gloss: string | null, options: AnswerOption[4] }`
**AnswerOption:** `{ optionId: number (0-3), text: string }`
**AnswerSubmission:** `{ sessionId: uuid, questionId: uuid, selectedOptionId: number (0-3) }`
**AnswerResult:** `{ questionId: uuid, isCorrect: boolean, correctOptionId: number (0-3), selectedOptionId: number (0-3) }`
### Error Handling
Typed error classes (`AppError` base, `ValidationError` 400, `NotFoundError` 404) with central error middleware. Controllers validate with `safeParse`, throw on failure, and call `next(error)` in the catch. The middleware maps `AppError` instances to HTTP status codes; unknown errors return 500.
### Key Design Rules
- Server-side answer evaluation: the correct answer is never sent to the frontend
- `POST` not `GET` for game start (configuration in request body)
- `safeParse` over `parse` (clean 400s, not raw Zod 500s)
- Session state stored in `GameSessionStore` (in-memory now, Valkey later)
---
## 9. Game Mechanics
- **Format**: source-language word prompt + 4 target-language choices
- **Distractors**: same POS, same difficulty, server-side, never the correct answer, never repeated within a session
- **Session length**: 3 or 10 questions (configurable)
- **Scoring**: +1 per correct answer (no speed bonus for MVP)
- **Timer**: none in singleplayer MVP
- **Auth required**: users must log in via Google or GitHub
- **Submit-before-send**: user selects, then confirms (prevents misclicks)
---
## 10. Working Methodology
This project is a learning exercise. The goal is to understand the code, not just to ship it.
### How to use an LLM for help
1. Paste this document as context
2. Describe what you're working on and what you're stuck on
3. Ask for hints, not solutions
### Refactoring workflow
After completing a task: share the code, ask what to refactor and why. The LLM should explain the concept, not write the implementation.
---
## 11. Post-MVP Ladder
<<<<<<< HEAD
| Phase | What it adds | Status |
| ----------------- | ------------------------------------------------------------------------------- | ------ |
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
=======
| Phase | What it adds | Status |
| ------------------- | ----------------------------------------------------------------------- | ------ |
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ✅ |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ✅ |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
> > > > > > > dev
### Future Data Model Extensions (deferred, additive)
- `noun_forms` — gender, singular, plural, articles per language
- `verb_forms` — conjugation tables per language
- `term_pronunciations` — IPA and audio URLs per language
- `user_decks` — which decks a user is studying
- `user_term_progress` — spaced repetition state per user/term/language
- `quiz_answers` — history log for stats
All are new tables referencing existing `terms` rows via FK. No existing schema changes required.
### Multiplayer Architecture (current + deferred)
**Implemented now:**
- WebSocket protocol uses the `ws` library with a Zod discriminated union for message types (defined in `packages/shared`)
- Room model uses human-readable codes (no matchmaking queue)
- Lobby flow (create/join/leave) is real-time over WS, backed by PostgreSQL for durable membership/state
- Multiplayer game flow is real-time: host starts, all players see the same question, answers are collected simultaneously, with a server-enforced 15s timer and live scoring
- WebSocket connections are authenticated (Better Auth session validation on upgrade)
**Deferred / hardening:**
- Valkey-backed ephemeral state (room/game/session store) where in-memory state becomes a bottleneck
- Graceful reconnect/resume flows and more robust failure handling (tracked in Phase 7)
### Infrastructure (current)
- `lilastudy.com` → React frontend (nginx serving static files)
- `api.lilastudy.com` → Express API + Better Auth
- `git.lilastudy.com` → Forgejo (git server + container registry)
- Docker Compose with Caddy for automatic HTTPS via Let's Encrypt
- CI/CD via Forgejo Actions (build on push to main, deploy via SSH)
- Daily DB backups with cron, synced to dev laptop
See `deployment.md` for full infrastructure documentation.
---
## 12. Definition of Done (Current Baseline)
- [x] API returns quiz terms with correct distractors
- [x] User can complete a quiz without errors
- [x] Score screen shows final result and a play-again option
- [x] App is usable on a mobile screen
- [x] No hardcoded data — everything comes from the database
- [x] Global error handler with typed error classes
- [x] Unit + integration tests for API
- [x] Auth works end-to-end (Google + GitHub via Better Auth)
- [x] Multiplayer works end-to-end (lobby + real-time game over WebSockets)
- [x] Production deployment is live behind HTTPS (Caddy) with CI/CD deploys via Forgejo Actions
---
## 13. Roadmap
See `roadmap.md` for the full roadmap with task-level checkboxes.
### Dependency Graph
```text
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)
```
---
## 14. Game Flow (Future)
Singleplayer: choose direction (en→it or it→en) → top-level category → part of speech → difficulty (A1C2) → round count → game starts.
**Top-level categories (post-MVP):**
- **Grammar** — practice nouns, verb conjugations, etc.
- **Media** — practice vocabulary from specific books, films, songs, etc.
- **Thematic** — animals, kitchen, etc. (requires category metadata research)