Skip to content
GitHub
Foundations

Security Boundaries

All security enforcement in Spectral happens at the API layer. The backend is the single trust boundary — every request to /api/* is authenticated and authorized by middleware before reaching any route handler.

This means:

  • Authentication is enforced by AuthMiddleware (JWT or API key validation)
  • Authorization is enforced by require_permission() dependencies on each route
  • Tenant isolation is enforced by RLS context set per-request with the authenticated workspace_id

No data leaves the database without passing through these controls.

The dashboard’s AuthGuard component (apps/dashboard/src/components/auth/auth-guard.tsx) checks whether the user has a valid session and redirects unauthenticated users to /login. This is a user experience gate, not a security control.

  1. No sensitive data in static HTML — page shells contain layout and UI components, not customer data
  2. All data fetches require authentication — every API call includes a bearer token validated server-side
  3. Guard prevents confusion, not attack — without it, unauthenticated users would see empty dashboards and broken UI rather than a clean login redirect
  • Do not rely on the frontend guard for access control. If a route should be restricted, enforce it with require_permission() on the API endpoint.
  • Do not embed sensitive data in page components. Data should always be fetched from authenticated API endpoints, never baked into the static build.
  • Treat the frontend as untrusted. Any value coming from the client (headers, query params, request bodies) must be validated server-side.

Behind reverse proxies (Cloudflare, Render), request.client.host returns the proxy’s IP, not the real client. The TrustedProxyMiddleware resolves the real client IP from X-Forwarded-For and X-Real-IP headers when the direct connection comes from a configured trusted proxy.

Configure trusted proxies via the TRUSTED_PROXIES environment variable (comma-separated IPs). When empty (default), proxy headers are ignored and the direct connection IP is used.

The resolved IP is available at request.state.client_ip for audit logging and rate limiting.