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_keystable with per-user isolation - Key resolution order: user DB key → server
.envkey → empty string - OneDrive tokens encrypted with Fernet symmetric encryption before database storage
- RLS (Row Level Security) policies on
user_api_keystable 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_ORIGINSenvironment variable
Notebook-Level Access Control¶
Notebook-scoped endpoints check that the user has access to the specific notebook:
- Ownership — Creator of the notebook
- Invite link — Redeemed invite grants
chat_onlyoradminaccess - 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.