Falsafa
SystemHigh-Level Design

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.

On this page