Workflow

Authentication Flow

How sign-in, session refresh, and per-request authorization actually work inside the app — end to end.

Owner: Security TeamLast reviewed: 2026-04-14

Provider

Supabase Auth

Cookie-based SSR · JWT access tokens

Session TTL

8 hours

Idle timeout · refresh rotates on use

Roles

9

Owner → Auditor → External Vendor

Rate limit

120 / min

Per IP · sliding window

Rendering diagram…
Sign-in and per-request authorization — happy and error paths.

Sign-in

  1. User submits email + password to Supabase Auth (POST /auth/v1/token, grant type password).
  2. Supabase verifies credentials and sets two HttpOnly, Secure, SameSite=Lax cookies: sb-access-token (short-lived JWT) and sb-refresh-token (rotating).
  3. Failed attempts advance the progressive lockout counter (see below).
  4. Browser is redirected to /dashboard; middleware re-validates and refreshes the session on the first request.

Progressive Lockout

Repeated failures for the same email + IP pair escalate through four lock windows. The counter resets after a successful sign-in or 24 hours of inactivity.

Rendering diagram…
Failure count → lock duration.

Per-Request Context

Every authenticated API route begins with requireOrgContext(), which returns the server-trusted identity:

  • userId — Supabase user id (UUID).
  • userEmail / userName — profile data joined from User.
  • orgId — the active organization the user has selected.
  • role — one of nine roles on the membership row.

The context is re-derived per request from the cookie — it is never accepted from the client body or headers. If the session is missing or expired the handler returns 401 immediately, before any RBAC or DB call.

Authorization (RBAC)

Handlers call requirePermission(ctx, action, entity) which consults a central role × entity matrix. The nine roles in descending privilege:

  • Owner · Admin — full org administration.
  • Compliance Officer · Risk Manager — cross-framework read/write.
  • Control Owner — write on owned entities; read elsewhere.
  • Member · Viewer — everyday contributor / read-only.
  • Auditor — scoped read of evidence packages and audit trail.
  • External Vendor — questionnaire responses only.

Denied actions return 403 with a machine-readable error code so the UI can render the correct empty or disabled state. Permission misses do not leak whether the entity exists.

Session Hygiene

  • Idle timeout — 8 hours; the next request after timeout forces re-auth.
  • Refresh rotation — every use of a refresh token issues a new pair and invalidates the old.
  • Explicit sign-out — revokes the refresh token server-side and clears both cookies.
  • Device revocation — Supabase Auth session list powers per-device sign-out from account settings.

What we never do

No authentication state is kept in localStorage. No bearer tokens are sent to third-party origins. Passwords are never logged, hashed locally, or passed to our own backend — they go directly to Supabase Auth.

Error Reference

  • 401 unauthenticated — missing / invalid session cookie.
  • 401 session_expired — cookie present but past TTL; client should re-auth.
  • 403 forbidden — authenticated but role lacks the permission.
  • 403 wrong_org — request targets an entity outside the active org.
  • 423 locked — account in a progressive lockout window.
  • 429 rate_limited — per-IP or per-endpoint budget exceeded.