Skip to content

Security Architecture


Authentication Flow

sequenceDiagram
    participant Browser
    participant ClerkSPA as Clerk (Frontend)
    participant Backend
    participant ClerkAPI as Clerk API (JWKS)

    Browser->>ClerkSPA: User signs in
    ClerkSPA-->>Browser: RS256 JWT (short-lived)

    Browser->>Backend: API request + Authorization: Bearer JWT
    Backend->>Backend: Extract JWT from header

    alt bypass_auth = true (dev mode)
        Backend->>Backend: Return stub user (user_id: "dev-user")
    else bypass_auth = false (production)
        Backend->>ClerkAPI: Fetch JWKS (cached 1h TTL)
        ClerkAPI-->>Backend: Public signing keys
        Backend->>Backend: Verify RS256 signature + expiry
        Backend->>Backend: Extract user_id from "sub" claim
    end

    Backend-->>Browser: Authenticated response

Authorization Model

Level Mechanism
Global admin users.role = 'admin' with 60s TTL cache
Notebook admin Owner or notebook_access.access_type = 'admin'
Notebook chat_only notebook_access.access_type = 'chat_only' (read + chat)
Bypass mode bypass_auth = true returns "admin" for all checks

API Key Security

  • User API keys stored in user_api_keys table with per-user isolation
  • Key resolution order: user DB key → server .env key → empty string
  • OneDrive tokens encrypted with Fernet symmetric encryption before database storage
  • RLS (Row Level Security) policies on user_api_keys table restrict access to the owning user

Transport Security

Caddy automatically adds security headers:

Header Value
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security max-age=31536000; includeSubDomains
X-XSS-Protection 1; mode=block
Server Removed (Caddy header stripping)

CORS

Configured via cors_origins in config.py:

  • Development: http://localhost:5173 (Vite), http://localhost:3000
  • Production: Set via the CORS_ORIGINS environment variable

Notebook-Level Access Control

Notebook-scoped endpoints check that the user has access to the specific notebook:

  1. Ownership — Creator of the notebook
  2. Invite link — Redeemed invite grants chat_only or admin access
  3. Admin override — Global admins can access all notebooks

The check_notebook_access() dependency returns "admin" or "chat_only" and raises HTTP 403 if no access exists.