The BFF trust boundary
How auth actually works, prose bound to the server code: the global guard, the httpOnly cookie session, and the authed client that refreshes on 401.
1 · System architecture — the BFF trust boundary
The single trust boundary is Donna's SvelteKit server. Server-only code lives under src/lib/server/
and is never imported into client code. Three files carry the whole boundary:
src/hooks.server.ts— the global guard. On every request it hydratesevent.locals.userby callingGET /api/v1/users/meif thedonna_atcookie is present (hooks.server.ts:9-14), then enforces routing: forced password rotation takes precedence (:27-30),(app)routes require a user or redirect to/login?next=(:31-34), and authed users are bounced off(auth)screens (:37-44).src/lib/server/lqClient.ts—lqFetch()attachesAuthorization: Bearer <donna_at>, and on a401refreshes once against/api/v1/auth/refreshusingdonna_rt, rotates both cookies, and retries the original request (lqClient.ts:19-47); if refresh fails it clears the session cookies and returns the original401.lqStream()is a single-attempt variant for SSE — no refresh, because the preceding pageloadalready ranlqFetchand guaranteed a fresh token (lqClient.ts:54-60).src/lib/server/session.ts— cookiesdonna_at/donna_rt, sethttpOnly,sameSite: 'lax',secure: !dev, with the refresh cookie pinned to an 8-hour TTL mirroring lq-ai's default (session.ts:4-20).
Pages get backend data through SvelteKit load (SSR); they mutate through form actions or small
BFF proxy routes (+server.ts). Proxy routes exist for two reasons: to attach auth, and to avoid
page/endpoint route collisions — e.g. /prompts/items sits beside the /prompts page, and the
/tabular-executions/[id] proxy is a separate top-level group precisely so it doesn't collide with the
/tabular/[executionId] page.
flowchart TB
subgraph client["Browser (no JWT, no direct backend access)"]
UI["Svelte 5 components · runes"]
end
subgraph bff["Donna SvelteKit server — the trust boundary"]
HOOKS["hooks.server.ts<br/>guard + user hydrate"]
LOAD["+page.server.ts<br/>load / form actions"]
PROXY["+server.ts<br/>27 BFF proxy routes"]
CLIENT["lqClient.ts<br/>lqFetch / lqStream"]
COOKIES[("httpOnly cookies<br/>donna_at · donna_rt")]
end
subgraph backend["vendored lq-ai (NEVER edited)"]
API["api · FastAPI"]
GW["gateway"]
WK["ingest-worker · arq-worker"]
PG[("postgres")]
REDIS[("redis")]
MINIO[("minio / S3")]
end
UI -->|"same-origin fetch / SSR"| HOOKS
UI --> LOAD
UI --> PROXY
HOOKS --> CLIENT
LOAD --> CLIENT
PROXY --> CLIENT
CLIENT -->|"Bearer + refresh-on-401"| API
CLIENT -. reads/writes .-> COOKIES
API --> PG
API --> REDIS
GW --> API
WK --> PG
API --> MINIO
The base URL is resolved server-side only, from LQ_API_INTERNAL_URL (src/lib/server/env.ts) — in
compose it is http://api:8000 (docker-compose.yml:38); in local dev http://localhost:18000. A
SvelteKit form-action POST needs an Origin header (CSRF); production sets ORIGIN on donna-web
(docker-compose.yml:35-36) or login returns 403.
Cardinal rule, enforced by structure: Donna consumes lq-ai's contract and never forks it. API types are generated from lq-ai's OpenAPI into
src/lib/api/backend.d.ts(10,891 lines) andgateway.d.ts(1,639 lines) vianpm run gen:api. The 205 distinct/api/v1/...endpoint shapes the BFF references all derive from there.