Core Docs

Session Architecture

Session Architecture



How sessions work in core.auth — JWT access tokens, stateful session validation, and the full lifecycle.

Design Decision



core.auth uses JWT access tokens with stateful session validation — a pragmatic hybrid that gives you stateless request verification today with a clean path to further optimization later.

  • Stateless access: JWT verification is local (HMAC signature check), no DB hit per request

  • Stateful refresh: session validity is checked against the DB only when the token expires

  • Simple revocation: delete the session row → user is logged out within the token's TTL

  • Standard format: JWT is universally supported, no custom token formats

The Architecture



ComponentTypeLifetimeVerificationPurpose



JWT access tokenStateless15 minutes (configurable)HMAC-SHA256 signature check, localProves identity for API requests
Session cookie (session_ref)Stateful reference30 days (configurable)DB lookup at core.authRenewing access tokens

Why This Over a Fully Stateful System?



A fully stateful system checks the database on every API request. That's secure but expensive — 100 requests per minute from a user means 100 DB queries per minute.

With JWT, API requests verify the token locally (signature + expiry check). The DB is only hit on refresh — once every 15 minutes per user. That's a ~100,000x reduction in DB load per active user.

Why Not a Fully Stateless System?



A fully stateless system (long-lived JWT, no DB) can't be revoked. If a token is stolen, it's valid until it expires. The session database gives you a revocation lever: delete the row, and the next refresh fails.

Session Lifecycle



1. User authenticates at core.auth


2. core.auth creates:
├── session record (DB)
└── JWT access token (HMAC-signed, returned to client)


3. core.auth sets:
├── Response body: { "access_token": "eyJ..." }
└── Set-Cookie: session_ref=<session-uuid>; HttpOnly; Secure; SameSite=Lax


4. App makes API requests with JWT access token
├── App verifies HMAC signature locally (no DB hit)
└── Token valid → proceed


5. JWT access token expires (15 min)


6. App sends refresh request via core.console (relayed to core.auth)
POST /session/refresh
Cookie: session_ref=<session-uuid> (sent automatically by browser)


7. core.auth validates session:
├── Session exists + not revoked + not expired → issue new JWT
└── Session missing/revoked/expired → 401, force re-authentication


8. Repeat from step 4


What Happens on Each Step



Step 1 — Authentication:
User provides credentials (password + optional 2FA). core.auth validates them against the users table.

Step 2 — Session + JWT creation:
core.auth creates a sessions record (tracking metadata, expiry) and signs a JWT access token containing the user ID, app slug, scopes, session ID, and expiry.

Step 3 — Token delivery:
The JWT is returned in the response body. The session reference is set as an HttpOnly, Secure, SameSite=Lax cookie on the core.auth domain. The cookie is the refresh credential — the client never needs to manage it.

Step 4 — API requests:
The app includes the JWT in requests (Authorization: Bearer <token> or cookie). The receiving service verifies the HMAC signature locally — no database lookup, no network call to core.auth.

Step 5 — Expiry:
JWT access tokens have a short TTL (default 15 minutes). After expiry, the app must refresh.

Step 6 — Refresh:
The app sends a request to core.auth's /session/refresh endpoint. For browser clients, the session_ref cookie goes along automatically. For mobile clients, the session ID is sent as a header (X-Session-Ref).

Step 7 — Session validation:
core.auth looks up the session row in the database. If the session exists, is not revoked, and is not expired, it issues a new JWT. If the session is missing or revoked, it returns 401 and the user must re-authenticate.

Step 8 — Continuity:
The cycle continues. The user stays logged in as long as their session row exists in the DB and they keep refreshing.

Why This Is Simple



There's no refresh token. No token rotation. No family tracking. No stolen-token detection chains. The session row is the single source of truth:

  • Session row exists → user is authenticated → issue new JWT

  • Session row deleted → user is logged out → next refresh fails

Revocation is a single DELETE or UPDATE on the sessions table.

Session Cookie



The session_ref cookie is the credential for the refresh endpoint. It's a reference to the session row — not a token, not a JWT, just a session UUID.

Properties




PropertyValueWhy






HttpOnlytrueJavaScript can't access it (XSS can't steal it)
SecuretrueHTTPS only
SameSiteLaxSent on top-level navigations, not cross-site requests
Path/session/refreshOnly sent to the refresh endpoint, not every request
Max-Age30 daysMatches the session lifetime

Mobile Clients



Native mobile apps don't handle cookies automatically. They store the session ID locally and send it as a header on refresh:

Browser:  Cookie: session_ref=abc-123
Mobile: X-Session-Ref: abc-123

Backend: check cookie first, fall back to header.


Same endpoint, different transport. The backend doesn't care where the session ID comes from.

Sticky Sessions (Persistent Logins)



Following the pattern set by Google, Facebook, and other major providers, core.auth supports persistent login — the "remember me" behavior users expect.

The session cookie is long-lived (up to 30 days by default), and as long as the user keeps refreshing their JWT, they stay logged in — until the hard session cap is reached.

Hard Session Cap



The hard cap forces re-authentication regardless of activity. Default: 30 days from initial authentication.

This balances convenience with security:
  • Convenience: users don't get randomly logged out during active use

  • Security: abandoned sessions eventually expire

Configuration Per App



registered_apps:
├── access_token_ttl: 900 (15 min default)
├── session_ttl: 2592000 (30 days default)
├── hard_session_cap: 2592000 (30 days from login)
└── allow_remember_me: true


Apps can configure their own TTLs within limits set by core.auth. A banking app might want 5-minute JWTs and 7-day hard caps. A personal todo app might want 15-minute JWTs and 90-day hard caps.

Silent Refresh



To avoid interrupting the user when the JWT expires, apps should implement silent refresh — automatically refreshing the token in the background before it expires.

JWT issued at T, expires at T+15min


App sets a timer for T+13min (2 min before expiry)


Timer fires → app sends refresh request to core.auth


New JWT received


App updates stored token


User never notices


This can be implemented as:
  • A JavaScript timer in the frontend

  • A middleware check in the backend that refreshes proactively

  • A combination (frontend for SPA, backend for server-rendered)

Multi-Session Support



A user can have multiple active sessions simultaneously — one per device/browser. Each session has its own:
  • session_id

  • IP address / user agent tracking

  • Independent expiry

Sessions are visible in the account dashboard. Users can see "where am I logged in" and revoke individual sessions or all sessions at once.

User at laptop (Chrome)  → session_1, IP: 1.2.3.4, last active: 5 min ago
User at phone (Safari) → session_2, IP: 5.6.7.8, last active: 2 hours ago
User at tablet (Firefox) → session_3, IP: 9.10.11.12, last active: 3 days ago


Scaling Path



This architecture is designed to evolve without changing the client-side contract:


PhaseWhat ChangesDB Load per UserMigration Effort




Phase 1 (now)Session check in PostgreSQL1 query per 15 min
Phase 2 (scale)Session check in Redis1 query per 15 min (sub-ms)Transparent swap, no client change
Phase 3 (optimize)Add refresh tokens with rotationNear zero (stateless refresh)Backend-only, client contract unchanged

Each phase only changes the backend. The frontend always does: "send JWT with requests, call refresh endpoint when JWT expires."




Next: Scope Model — three-tier scoping system.