lila/documentation/spec.md
2026-04-15 05:16:29 +02:00

17 KiB
Raw Permalink Blame History

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 long-term vision is a multiplayer competitive game, but the MVP is a polished singleplayer experience.

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 MVP deliberately ignores most of it.


3. MVP Scope

Goal: A working, presentable singleplayer quiz that can be shown to real people.

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
  • Local dev only (no deployment for MVP)

What is CUT from the MVP

Feature Why cut
Authentication (Better Auth) No user accounts needed for a demo
Multiplayer (WebSockets, rooms) Core quiz works without it
Valkey / Redis cache Only needed for multiplayer room state
Deployment to Hetzner Ship to people locally first
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) post-MVP
Cache Valkey post-MVP

5. Repository Structure

vocab-trainer/
├── .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
├── docker-compose.prod.yml             — production config reference
├── 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

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

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

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

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

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 (deferred)

  • WebSocket protocol: ws library, Zod discriminated union for message types
  • Room model: human-readable codes (e.g. WOLF-42), not matchmaking queue
  • Game mechanic: simultaneous answers, 15-second server timer, all players see same question
  • Valkey for ephemeral room state, PostgreSQL for durable records

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 (MVP)

  • API returns quiz terms with correct distractors
  • User can complete a quiz without errors
  • Score screen shows final result and a play-again option
  • App is usable on a mobile screen
  • No hardcoded data — everything comes from the database
  • Global error handler with typed error classes
  • Unit + integration tests for API

13. Roadmap

See roadmap.md for the full roadmap with task-level checkboxes.

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)

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)