Repository navigation
Architecture Overview
Deutsch· is a React 18 + Vite 8 PWA with an optional server side. The defining constraint is that the app is fully usable with no account and no network, and everything else is arranged around that.
The system splits into two lanes that meet only at well-defined seams.
The local lane is the authority. localStorage holds SRS state, daily
stats, streaks, learned words, decks, and settings. Every write lands here
first, synchronously, before anything touches a network. This is what makes the
app work on a train.
The remote lane is optional and additive. Supabase provides auth, Postgres, and avatar storage; Vercel Functions provide a versioned API. Signing in adds cross-device sync, leagues, and a portable profile — it never becomes a precondition for learning.
flowchart LR
subgraph Browser["Browser · works offline"]
UI["React 18 UI"]
Local[("localStorage")]
SW["PWA service worker"]
UI <--> Local
UI <--> SW
end
subgraph Vercel["Vercel"]
API["Versioned serverless API\nai · content · progress · league\nsocial · account · admin · push"]
end
subgraph Supabase["Supabase"]
Auth["Auth"]
DB[("Postgres + RLS\nprogress · sync · quota accounting")]
Storage[("Avatar Storage")]
end
UI --> API --> DB
UI -.->|"PostgREST"| DB
UI -.-> Auth
UI -.-> Storage
Merges happen on the client, never on the server. Keeping reconciliation in pure functions makes it deterministic, unit-testable without a database, and — critically — usable before the network returns. The server stores reconciled state; it does not arbitrate it. See Offline-First Sync Model.
The one exception is progress: server-side progress writes go through a single Postgres RPC so the database has exactly one writer. See Lesson Engine.
Code in src/lib/* and src/components/* is language-blind. No
if (language === 'de'), no German strings, no German grammar baked into
engine logic. German-specific behaviour lives in src/packs/de/ and is reached
through activePack.
There is one recorded exception: pack cards ship as { de, en, ipa, … }, and
several components read card.de directly. That is a field name in the pack's
own data contract, not a German branch — nothing reads it to decide German
behaviour. AGENTS.md records why it should not be "fixed".
All browser-to-server calls go to versioned, same-origin endpoints under
/api/v1/. Contracts live in
docs/api/.
| Route group | Purpose |
|---|---|
/api/v1/ai/* |
chat, grade, deck — model routing, quota accounting, and Anthropic |
/api/v1/content/* |
lessons — the data-driven content lane |
/api/v1/progress/* |
events, daily — queued progress, RPC-backed |
/api/v1/league/* |
join, profile, handle, refresh, settle
|
/api/v1/account/* |
profile, export, delete
|
/api/v1/social/* |
search, follow lists, and profile visibility |
/api/v1/admin/* |
authenticated operational controls |
/api/v1/push/* |
native push-device registration and reminders |
Every non-2xx response uses one error envelope:
{ "error": { "code": "<machine_code>", "message": "<human text>" } }Breaking changes mean /api/v2/; shipped /v1 contracts stay stable.
| Layer | Technology | Role |
|---|---|---|
| UI | React 18, Vite 8, Capacitor 8 | Shared web UI, PWA build, and native iOS/Android shells |
| Offline |
PWA / Workbox, localStorage
|
App-shell caching and local-first learner state |
| Data | Supabase Postgres | Account data, progress, sync, leagues, entitlements, and AI usage |
| Data API | PostgREST via Supabase | RLS-protected browser reads and writes |
| Identity & media | Supabase Auth + Storage | Email/OAuth sessions, legal acceptance, and user-owned avatars |
| Server | Vercel Functions | Versioned AI, content, progress, league, social, account, and push endpoints |
| AI | Anthropic Claude | Cost-aware conversation, deck generation, and B1 grading |
| Quality | Vitest, React Testing Library, Playwright, ESLint, Prettier | Unit, integration, policy, contrast, lint, and format checks |
Styling is inline only, with tokens from src/lib/theme.js. Hardcoded
colors, radii, and shadows are rejected by a source test. Four vendored
typefaces have separate roles: Fraunces for display, Plus Jakarta Sans for body,
JetBrains Mono for UI labels, and Noto Sans Mono for IPA. The IPA subset is built
from the characters actually present in the German content and is verified by
fontCoverage.test.js.
api/ Vercel functions and shared server middleware
_lib/ Handlers, validation, rate limiting, auth middleware
v1/ Versioned routes: ai, content, progress, league, social, account, admin, push
src/components/ Learning surfaces and accessible UI primitives
exercises/ The exercise registry and its renderers
ui/ Design-system primitives
src/lib/ SRS, sync, gamification, quests, placement, auth, avatars
src/packs/de/ German content-pack behavior
supabase/migrations/ Schema, grants, RLS policies, functions, and Storage
supabase/tests/rls/ Adversarial PostgREST policy tests
scripts/ Lexicon import, lesson seeding, font vendoring, audits
docs/api/ Versioned API contracts
docs/superpowers/ Design specs and implementation plans
Per-mission specs and plans are in
docs/superpowers/.
They are dated records of what was decided and why, kept in the repo so they are
reviewable next to the code they describe. They are not living documents —
when a spec and the code disagree, the code is the truth and the spec is
history.
Design
Working on it