Decisions (ADRs)ADR-002 · JWT + refresh auth

ADR-002 · JWT access tokens + refresh tokens

Status: Accepted

Context

The API serves three clients — two Next.js web apps and an Expo mobile app — and must stay stateless enough to run behind a load balancer without sticky sessions, while still supporting logout, session revocation, and short-lived credentials for security. Mobile clients also need to stay logged in for long stretches without re-entering a password.

Decision

Use short-lived JWT access tokens plus long-lived refresh tokens:

  • Access token — 15 minutes, sent as Authorization: Bearer on every request. Carries user_id, tenant_id, email, is_platform_admin.
  • Refresh token — 30 days, stored in an HTTP-only cookie (web) or SecureStorage (mobile), exchanged at POST /api/auth/refresh (via X-Refresh-Token) for a fresh access token.
  • Sessions table tracks issued tokens (hash + expiry) so logout/password-change can revoke them — giving stateful control on top of stateless tokens.
  • Passwords hashed with bcrypt (cost ≥ 12); auth endpoints rate-limited.

Consequences

Positive

  • Requests verify cheaply (signature check), no session store lookup on the hot path.
  • Short access-token lifetime limits the blast radius of a leaked token.
  • The sessions table restores revocation, so “log out everywhere” and “revoke on password change” work despite stateless tokens.

Negative / cost

  • Two-token flow adds client complexity (refresh handling, retry-on-401).
  • Rotating JWT_SECRET forces all users to re-login — must be announced.
  • Revocation depends on the sessions table being checked/maintained; expired sessions need a scheduled cleanup.

Alternatives considered

  • Server-side sessions only — simpler revocation, but a session lookup per request and sticky-session or shared-store overhead across three clients.
  • Long-lived access tokens, no refresh — simpler, but a leaked token stays valid too long.