Architecture
Coachly is a German AI career-coaching demo built to make its engineering legible: a multi-tenant app where the database — not application code — enforces isolation, conversation state is owned rather than rented, and the AI layer is swappable and testable.
Request & data flow
1. Browser (SvelteKit, Svelte 5 runes)
Sends cookies; renders streamed responses.
2. Server hooks
Attaches a cookie-backed Supabase client; revalidates the JWT against GoTrue (getUser), not just the cookie.
3. load / +server / actions
Each self-gates: requireUser (and requireCoach for staff). Identity comes from the session, never request input.
4. User-scoped Supabase client
Every query runs as the authenticated user — no service-role in the request path (except server-owned usage caps).
5. Postgres + Row Level Security
The database enforces per-tenant isolation on every table and storage object.
Every hop runs as the authenticated user; RLS is the backstop.
Three RLS surfaces
Per-user table RLS
Every table (profiles, chat, assistant data) has policies scoped to the authenticated role and keyed on auth.uid(). No USING (true) — ever.
Owner-folder storage RLS
Private buckets (avatars, CVs, vision boards) key each object to its owner via the first path segment; the server derives the path from the session; reads use short-lived signed URLs.
Coach second RLS surface
A coach_users membership table + a SECURITY DEFINER is_coach() function drive additive policies. A coach reads clients and mints codes through the normal client — the database widens access — with no service-role in the coach path and no self-promotion.
Owned state & the AI layer
Conversation history lives in our own chat_threads / chat_messages tables (not a vendor's managed threads), which is what lets the AI provider be swapped freely. An AIProvider interface has a real streaming implementation and a deterministic mock selected by env, so CI and E2E run with no live keys and stable output. Function-calling tool schemas and argument validation are defined in our code.
Decision records
- ADR-0001: Own the conversation state
- ADR-0002: RLS scoped to authenticated, never USING (true)
- ADR-0003: Self-host Supabase on a Hetzner VM
- ADR-0004: Model-agnostic AI provider with a deterministic mock
- ADR-0005: Private storage with owner-folder RLS
- ADR-0006: Session-derived identity gates
- ADR-0007: Coach dashboard as a second RLS surface
Full ADRs live in docs/adr/ in the repository.