| .forgejo/workflows | ||
| apps | ||
| data-sources | ||
| documentation | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| Caddyfile | ||
| docker-compose.yml | ||
| eslint.config.mjs | ||
| mise.toml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
lila
Learn words. Beat friends.
lila is a vocabulary trainer built around a Duolingo-style quiz loop: a word appears in one language, you pick the correct translation from four choices. It supports singleplayer and real-time multiplayer, and is designed to work across multiple language pairs without schema changes.
Live at lilastudy.com.
Stack
| Layer | Technology |
|---|---|
| Monorepo | pnpm workspaces |
| Frontend | React 18, Vite, TypeScript |
| Routing | TanStack Router |
| Server state | TanStack Query |
| Styling | Tailwind CSS |
| Backend | Node.js, Express, TypeScript |
| Database | PostgreSQL + Drizzle ORM |
| Validation | Zod (shared schemas) |
| Auth | Better Auth (Google + GitHub) |
| Realtime | WebSockets (ws library) |
| Testing | Vitest, supertest |
| Deployment | Docker Compose, Caddy, Hetzner VPS |
| CI/CD | Forgejo Actions |
Repository Structure
lila/
├── apps/
│ ├── api/ — Express backend
│ └── web/ — React frontend
├── packages/
│ ├── shared/ — Zod schemas and types shared between frontend and backend
│ └── db/ — Drizzle schema, migrations, models, seeding scripts
├── scripts/ — Python scripts for vocabulary data extraction
└── documentation/ — Project docs
packages/shared is the contract between frontend and backend. All request/response shapes are defined there as Zod schemas and never duplicated.
Architecture
Requests flow through a strict layered architecture:
HTTP Request → Router → Controller → Service → Model → Database
Each layer only talks to the layer directly below it. Controllers handle HTTP only. Services contain business logic only. Models contain database queries only. All database code lives in packages/db — the API never imports Drizzle directly for queries.
Data Model
Words are modelled as language-neutral concepts (terms) with per-language translations. Adding a new language requires no schema changes — only new rows. CEFR levels (A1–C2) are stored per translation for difficulty filtering.
Core tables: terms, translations, term_glosses, decks, deck_terms
Auth tables (managed by Better Auth): user, session, account, verification
Vocabulary data is sourced from WordNet and the Open Multilingual Wordnet (OMW).
API
POST /api/v1/game/start — start a quiz session (auth required)
POST /api/v1/game/answer — submit an answer (auth required)
GET /api/v1/health — health check (public)
ALL /api/auth/* — Better Auth handlers (public)
The correct answer is never sent to the frontend — all evaluation happens server-side.
Multiplayer
Rooms are created via REST, then managed over WebSockets. Messages are typed via a Zod discriminated union. The host starts the game; all players answer simultaneously with a 15-second server-enforced timer. Room state is held in-memory (Valkey deferred).
Infrastructure
Internet → Caddy (HTTPS)
├── lilastudy.com → web (nginx, static files)
├── api.lilastudy.com → api (Express)
└── git.lilastudy.com → Forgejo (git + registry)
Deployed on a Hetzner VPS (Debian 13, ARM64). Images are built cross-compiled for ARM64 and pushed to the Forgejo container registry. CI/CD runs via Forgejo Actions on push to main. Daily database backups are synced to the dev laptop via rsync.
See documentation/deployment.md for the full infrastructure setup.
Local Development
Prerequisites
- Node.js 20+
- pnpm 9+
- Docker + Docker Compose
Setup
# Install dependencies
pnpm install
# Create your local env file (used by docker compose + the API)
cp .env.example .env
# Start local services (PostgreSQL, Valkey)
docker compose up -d
# Build shared packages
pnpm --filter @lila/shared build
pnpm --filter @lila/db build
# Run migrations and seed data
pnpm --filter @lila/db migrate
pnpm --filter @lila/db seed
# Start dev servers
pnpm dev
The API runs on http://localhost:3000 and the frontend on http://localhost:5173.
Testing
# All tests
pnpm test
# API only
pnpm --filter api test
# Frontend only
pnpm --filter web test
Roadmap
| Phase | Description | Status |
|---|---|---|
| 0 | Foundation — monorepo, tooling, dev environment | ✅ |
| 1 | Vocabulary data pipeline + REST API | ✅ |
| 2 | Singleplayer quiz UI | ✅ |
| 3 | Auth (Google + GitHub) | ✅ |
| 4 | Multiplayer lobby (WebSockets) | ✅ |
| 5 | Multiplayer game (real-time, server timer) | ✅ |
| 6 | Production deployment + CI/CD | ✅ |
| 7 | Hardening (rate limiting, error boundaries, monitoring, accessibility) | 🔄 |
See documentation/roadmap.md for task-level detail.