Request Pipeline
The shared authentication, authorization, and rate-limit gates every API request passes.
Every API request runs the same gauntlet before any business logic executes. These gates are single, reusable implementations — not per-route code — so security behaves consistently across the whole surface.
The gates
Middleware — checks session presence, applies the Content Security Policy, and short-circuits during maintenance mode.
Authentication — verifies the Firebase session cookie (or an internal
JWT for service-to-service calls) and reconciles the database user. Fails with
401.
Authorization (RBAC) — resolves the caller's highest grant for the
target entity (read < write < admin), scoped to the workspace. Fails with
403.
Rate limiting — a sliding-window counter on expensive and public
routes. Fails with 429.
Handler — the validated request reaches Zod-checked business logic.
Request flow
sequenceDiagram
autonumber
actor C as Caller
participant E as Edge
participant G as API Guards
participant A as Auth
participant Z as RBAC
participant L as Rate Limiter
participant H as Handler
C->>E: HTTPS request
E->>G: Route handler
G->>A: requireApiAuth()
A-->>G: session or null
alt not authenticated
G-->>C: 401 Unauthorized
else authenticated
G->>Z: requireWorkspaceAccess(min)
Z-->>G: permission or null
alt insufficient permission
G-->>C: 403 Forbidden
else authorized
G->>L: checkRateLimit(key)
alt limit exceeded
L-->>C: 429 Too Many Requests
else within limit
G->>H: validated request
H-->>C: 200 + data
end
end
endRate-limit store
The limiter uses a process-local in-memory window by default. When a Redis URL is configured, it switches to a Memorystore-backed sliding window (atomic Lua) shared across all server instances, so a single user can't exceed their limit by spreading requests across replicas.
The Redis client connects lazily and the limiter fails open to memory — a cache outage can never crash startup or 500 a request. It degrades to per-instance limits until Redis recovers.
Tenant isolation
Authorization is always workspace-scoped. A grant is a row keyed by
(userId, entityType, entityId), and the executor threads userId and workspaceId
through the entire run — so a workflow can only ever read and write data inside its own
tenant boundary. See Multi-tenancy.