API Documentation
17 route domains, auth patterns, rate limiting, tier-gating matrix, mobile-app surface, streaming pointer, security headers. No per-endpoint table — see §9.
Status: Current as of 2026-04-18 (repo commit 06d84cb).
Audience: New contributors, mobile team, third-party integrator, BfArM reviewer.
Authoritative source: apps/web/app/api/**. This doc describes patterns and domains, not every route — a hand-maintained per-endpoint table would drift on the first PR. §9 proposes auto-generation.
1. Domains
17 top-level route directories under apps/web/app/api/:
| Domain | Purpose | Auth pattern |
|---|---|---|
admin/ | Admin-only actions (moderation, user management) | Supabase JWT + admin role check |
chat/ | Julia AI chat — main + anon, history, export, cleanup (cron) | Mixed (public anon, JWT auth, CRON_SECRET) |
community/ | Forum posts, comments, categories | Supabase JWT |
documents/ | PHI document CRUD, Textract, simplification, export, share | Supabase JWT + document_storage feature gate |
emails/ | Transactional email sends (Resend) | Service-role / internal |
feature-flags/ | Client fetches enabled flags | Public or JWT (role-aware payload) |
inbox/ | User notifications + messages | Supabase JWT |
kliniken/ | Breast-center finder (DKG data) | Supabase JWT + klinik_finder (Premium) |
matchmaking/ | Breastfriend peer matching | Supabase JWT + breastfriend_matching (Premium) |
moderation/ | Admin moderation flows + content analyze | Supabase JWT + admin role |
newsletter/ | Subscribe / unsubscribe / confirm double-opt-in | Public + token-signed links |
profil/ | User profile read/update | Supabase JWT (self) |
stripe/ | Checkout, webhook, portal | Mixed (webhook = Stripe signature, others = JWT) |
studies/ | Clinical trial matching + health (data staleness) | Supabase JWT + studien_matching (Premium), health public |
timeline/ | Behandlungszeitstrahl events | Supabase JWT + behandlungszeitstrahl (Premium) |
unterlagen/ | Document sharing layer on top of documents/ | Supabase JWT + document_storage |
voice/ | LiveKit token mint + session metadata | Supabase JWT |
(Quilt §7 counted 14 domains. Delta: emails/, feature-flags/ are new; moderation/ was folded into admin/ in quilt.)
Total route files as of 2026-04-18: on the order of 55–60 across these directories. The exact count moves on every feature PR — trust git ls-files 'apps/web/app/api/**/route.ts' | wc -l over this doc.
2. Auth patterns
Four distinct patterns. Knowing which pattern a route uses tells you how to call it.
2.1 Supabase JWT Bearer (most common)
Authorization: Bearer <supabase-access-token>- Token obtained from Supabase Auth session on the client
- Server-side helper:
createClient()inapps/web/lib/supabase/server.ts→getUser() - Middleware:
apps/web/lib/supabase/middleware.tsrefreshes session + sets security headers - On missing / invalid: return 401 with
{error: "Nicht authentifiziert"}(German messaging is consistent)
2.2 Admin-only
- Still uses Supabase JWT, but route additionally checks a role in
user_roles(or similar) table - Pattern:
if (!isAdmin(user)) return 403 - Current admin surfaces:
admin/,moderation/, admin branches ofcommunity/
2.3 Public + Turnstile (planned, partial)
chat/anon/uses a session cookie (fj_anon_chat_sid) + IP-based quotas, no JWT- Turnstile bot-check is planned per #358 — not yet in
.env.exampleor route handlers - Once landed: public routes get a
cf-turnstile-responsebody field verified server-side
2.4 CRON_SECRET
chat/cleanup(invoked by Vercel cron at 03:00 UTC)- Guard:
Authorization: Bearer ${CRON_SECRET}— Vercel sets this automatically - Any new cron route must include this guard — don't rely on "nobody knows the path"
2.5 Stripe webhook signature (special)
stripe/webhookverifiesStripe-Signatureheader againstSTRIPE_WEBHOOK_SECRET- Returns 400 if signature invalid — never processes the event payload before verification
3. Rate limiting
Implementation: Upstash Redis sliding-window via apps/web/lib/rate-limit.ts.
Standard envelope on rate-limited routes:
headers:
X-RateLimit-Limit: <N>
X-RateLimit-Remaining: <N - 1>
X-RateLimit-Reset: <unix-seconds>Concrete limits (verified):
| Route | Limit | Window | Keyed on |
|---|---|---|---|
POST /api/chat | 20 | 60 s | client IP |
POST /api/chat/anon | 10 per session + 3 sessions/IP/day | session + 24 h | cookie + IP |
Most other routes rely on Vercel edge + Supabase's built-in abuse protection. Formal rate limits are not yet applied to document CRUD, community, or inbox — flag as a hardening item.
Failure mode: Upstash outage → rate limiter fails open (documented in architecture §5). Trade-off: availability over strict enforcement. Not a DiGA blocker, but surface if asked.
4. Tier gating matrix
Which domains' routes call checkFeatureAccess() (server-side) — derived from apps/web/lib/subscription/types.ts:
| Feature | Required tier | Enforcement site |
|---|---|---|
chat_unlimited | Free | No gate (available to all authed users) |
chat_history_7d / _full | Free / Plus | chat/history response filtering |
document_storage | Plus | documents/*, chat (master.md injection), unterlagen/* |
arztbrief_simplify | Plus | documents/simplify |
pdf_export | Plus | documents/export, chat/export |
community_full | Plus | community/* write endpoints (read is free) |
breastfriend_matching | Premium | matchmaking/* |
klinik_finder | Premium | kliniken/* |
studien_matching | Premium | studies/* (except studies/health public) |
behandlungszeitstrahl | Premium | timeline/* |
Full mechanics in subscription-auth-system.
5. Mobile app surface
The Expo app (apps/mobile/) currently uses only a small subset of the endpoints above. Known consumers:
voice/*— LiveKit token mint + session metadata- Selected
chat/*— via the shared API client inpackages/shared/
The shared createApiClient in packages/shared/ currently wraps ~7 endpoints. The remaining ~48 are web-only.
Gap to close if mobile feature-parity is a goal:
- Community posting
- Document upload (needs native file picker integration)
- Timeline, kliniken, studien
- Inbox
Track each as a separate issue — this doc just names the gap, doesn't schedule the work.
6. Request / response conventions
- Content type: JSON for everything except streaming (
text/event-streamfor/api/chat) - Error envelope:
{ "error": "Human-readable German message" }for user-facing 4xx, generic{ "error": "Ein Fehler ist aufgetreten" }for 5xx - Success envelope: resource-shaped (no global wrapper)
- Pagination: cursor-based where needed,
?cursor=...&limit=...(enumerate as per-endpoint comes up) - Idempotency: not systematically enforced — document per-route as gotchas appear
7. Streaming (/api/chat)
Separate doc covers this in full: streaming.
Quick pointer:
- AI SDK v6
streamTextviaDefaultChatTransport - SSE response (
result.toUIMessageStreamResponse()) - Client consumes via
useChatfrom@ai-sdk/react - Server
onFinishhook is currently stubbed — persistence is client-side. Tab-close during stream = messages lost.
8. Security headers
Applied by middleware (apps/web/lib/supabase/middleware.ts):
Strict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-Options: DENY- Referrer / Permissions-Policy baseline
Specific endpoints may layer additional headers (CSP is configured at the Vercel / Next config level — verify against next.config.mjs when hardening).
9. Why there's no per-endpoint table here
A hand-maintained table of 55+ endpoints with method / path / auth / rate-limit columns would be wrong on day one and get worse every PR. Two better options:
Option A — Auto-generate from zod + route introspection (recommended)
- Introduce zod schemas for request / response of each route (many already exist for validation)
- Use
zod-to-openapito produce/api/openapi.jsonat build time - Serve Swagger UI at
/api/docs(internal auth gate — admin-only) - One source of truth: the route code itself. No drift.
- Estimated effort: ~1 week to retrofit the top 15 routes, incremental after
Option B — Filesystem scanner
- Script walks
apps/web/app/api/**/route.ts, extracts exported HTTP verbs, returns a tabular MD block - Committed as
docs/dev/api-routes.generated.md - Pre-commit hook regenerates on touch to
app/api/** - Simpler than Option A, but doesn't give you request / response shapes
Recommendation: Option A. Zod schemas already exist for several routes; formalizing them delivers validation + docs in one pass.
10. Related
| # | Relevance |
|---|---|
| #579 | Parent docs epic |
| #586 | Pillar B parent |
| #358 | Adds Turnstile + planned guardrail_events route |
| architecture | Higher-level system view |
| subscription-auth-system | Source for §4 tier matrix |
| audit-system | Will wrap most routes in §3 audit events |
apps/web/app/api/ | Authoritative route tree |
apps/web/lib/rate-limit.ts | §3 implementation |
apps/web/lib/supabase/middleware.ts | §2.1 + §8 |
Changelog
- 2026-04-18 — Initial version. 17 domains identified (quilt §7 said 14; new:
emails/,feature-flags/;moderation/split out fromadmin/). Per-endpoint table intentionally omitted in favor of OpenAPI auto-gen recommendation (§9). Rate-limit config verified for/api/chat(20/60s); other routes unbounded.
Configuration System
Env var inventory across web (Vercel), voice (EC2), and mobile (EAS). Security classification, cron definition, rotation runbook, known gaps.
Streaming (Text Chat)
AI SDK v6 stack, streamText config, the stubbed server onFinish gap, chat history tiers, anon→auth handoff, provider migration (#358).