Trust Boundaries
Where user identity is validated across each hop: browser to frontend, frontend to backend, and backend to external APIs.
Trust Boundaries
Each arrow in the topology diagram represents a trust boundary. The system passes user identity across three of them, with different validation at each hop.
Boundary 1: Browser to Frontend
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ Cookie: sb-{project-ref}-auth-token=... │
│ Body: { session_id: "...", message: "..." } │
│ Headers: X-User-Id, X-API-Key (sent by Next.js route) │
└────────────────────┬────────────────────────────────────┘
│ HTTP POST /api/chat
▼
┌─────────────────────────────────────────────────────────┐
│ Next.js Middleware │
│ 1. Read Supabase session cookie from request │
│ 2. Call supabase.auth.getUser() to validate token │
│ 3. Query profiles.status to check blocked │
│ 4. Allow or redirect to login │
└─────────────────────────────────────────────────────────┘The frontend validates the Supabase session cookie on every request via the edge middleware. API routes call auth() which re-validates the cookie server-side and enriches the user object with role and status from the user_roles and profiles tables using the admin client. The frontend trusts the cookie because Supabase Auth signed it with the anon key, and the admin client reads with the service-role key which bypasses RLS.
Boundary 2: Frontend to Backend
┌─────────────────────────────────────────────────────────┐
│ Frontend (Next.js API Route) │
│ Proxy request after internal validation: │
│ - Session cookie validated by auth() │
│ - Chat session ownership checked │
│ - Library ownership confirmed │
│ │
│ Outgoing payload: │
│ POST http://backend:8001/character/chat │
│ Headers: X-User-Id: <validated_user_id> │
│ Body: { │
│ "user_id": "<validated uuid>", │
│ "character_id": "<uuid>", │
│ "book_id": "<uuid>", │
│ "session_id": "<uuid>", │
│ "user_message": "..." │
│ } │
└────────────────────┬────────────────────────────────────┘
│ (Docker internal network)
▼
┌─────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ No application-layer auth. No API key validation. │
│ Assumes validation already happened on the frontend: │
│ - Does NOT verify auth token │
│ - Does NOT check library ownership │
│ - Does not validate X-User-Id or X-API-Key headers │
│ - Trusts user_id, character_id, book_id as-is │
│ - Validates only that session_id exists in DB and │
│ its user_id matches (last-chance guard) │
└─────────────────────────────────────────────────────────┘The backend has no application-layer authentication. It trusts the frontend implicitly because they share a Docker network. The frontend validates ownership and authentication before proxying any request. The backend's only defense is re-checking that the session user_id matches the request user_id - a last-chance guard against misrouting, not an authentication boundary. The X-User-Id header sent by the frontend is advisory; the backend reads user_id from the JSON body.
Boundary 3: Backend to External APIs
┌─────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ Outgoing calls use per-provider API keys from config: │
│ - LLM: OPENAI_API_KEY / ANTHROPIC_API_KEY / OLLAMA_URL │
│ - Embedding: OPENAI_API_KEY / COHERE_API_KEY / ... │
│ - Reranker: RERANKER_API_KEY │
│ - Supabase: SUPABASE_SERVICE_ROLE_KEY │
│ │
│ No user identity is forwarded to these APIs. │
│ All calls are made as the application itself. │
└─────────────────────────────────────────────────────────┘The backend does not forward any user identity to external APIs. LLM calls are made with the application's API key, not user tokens. This means usage cannot be attributed per-user at the provider level.