Skip to content

Architecture Overview

Shimon Esterkin edited this page Oct 6, 2026 · 2 revisions

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 two lanes

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
Loading

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.

Engine and pack

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".

The API surface

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.

Tech stack

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.

Repository map

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

Where the design history lives

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.

Clone this wiki locally