Raxx · internal docs

internal · gated ↑ index

Session engine — REST + JWT + server-side revocation + per-tier rate limits

Status: Draft Owner: software-architect Last updated: 2026-04-22 Parent epic: #183 Related: auth.md, mbt-paper-trading-engine.md, ADR 0001, ADR 0002, ADR 0013


1. Context + goal

auth.md specifies the WebAuthn flow and a sessions table. This doc pins down the operational shape: REST endpoints for session lifecycle, JWT token format, server-side revocation, and per-tier rate limits per the pricing matrix (Free 60/min, Pro 600/min, Pro+ 3000/min).

Goal: session management that is a first-class REST surface (consumable by browser, iOS, console, and CLI) while preserving instant revocation and per-tier enforcement.

2. Invariants

All auth.md §2 invariants apply. Session-specific:

3. Endpoints

All under /api/sessions/*. Authentication on protected endpoints is via the session cookie (primary) or Authorization: Bearer <jwt> header (secondary, for non-browser clients).

Method Path Purpose
POST /api/sessions Create a new session from a WebAuthn assertion. Body: {assertion}. Response: session cookie set + JSON body with token details (no raw JWT in browser flow; cookie only).
GET /api/sessions/current Return the caller's session metadata: {user_id, role, tier, issued_at, expires_at, fresh_until}. No token material returned.
DELETE /api/sessions/current Revoke the current session (logout). Cookie is cleared + session row revoked_at is stamped.
POST /api/sessions/refresh Roll the session within its rolling window. Requires the cookie; does not require a fresh assertion unless the 24h step-up window has lapsed.
GET /api/sessions List the caller's active sessions (multi-device). Returns session metadata with ip_prefix, user_agent, last_seen_at.
DELETE /api/sessions/{id} Revoke a specific session (remote logout of another device). Requires step-up assertion.
POST /api/sessions/step-up Refresh the fresh_until timestamp on the current session using a new WebAuthn assertion. Used before live-trade actions and credential changes.

Create / step-up endpoints are rate-limited more strictly than data endpoints (per-IP + per-email bucket) to block brute-force and enumeration.

4. Token format

In the browser (primary path)

For non-browser clients (iOS, CLI, console)

Validation order

Every authenticated request:

  1. Parse cookie → look up session_id (hashed) in the session store (Redis).
  2. If not present, try Authorization: Bearer <jwt>: verify signature, extract sid, look up in Redis.
  3. Check revoked_at IS NULL and expires_at > now.
  4. Attach {user_id, tier, role, session_id, fresh_until} to the request context.

If any check fails: 401 with WWW-Authenticate: Session (browser) or WWW-Authenticate: Bearer (non-browser). No error message leakage beyond "session invalid."

5. Server-side session store

sessions lives in Redis for O(1) lookup + TTL-based expiry. Persistent shadow copy in Postgres/SQLite for audit and cross-region replay. Schema per auth.md §3 extended:

sessions (Redis key: session:<sid_hash>)
  user_id            string
  credential_id      string
  tier               'free'|'pro'|'pro_plus'
  role               'user'|'admin'
  issued_at          timestamp
  expires_at         timestamp (TTL on Redis key)
  fresh_until        timestamp           -- updated on step-up
  revoked_at         timestamp | null
  last_seen_at       timestamp
  ip_prefix          string              -- /24 or /48
  user_agent         string

Revocation is DEL session:<sid_hash> in Redis + update the shadow row. The next request fails lookup → 401.

Tier pinning: the tier on the session is captured at mint time and refreshed only on POST /api/sessions/refresh. A user who upgrades mid-session must refresh to see new rate-limit headroom; this is a UX note (Antlers triggers refresh after a successful upgrade).

6. Per-tier rate limits

Per docs/marketing/pricing.md:

Tier Rate limit
Free 60 req/min
Pro 600 req/min
Pro+ 3,000 req/min

Enforcement

Exceptions

7. Step-up flow

Any route decorated @require_fresh_assertion(max_age_minutes=5):

Step-up endpoints are: live-trading actions, credential add/remove, GDPR erasure, admin actions, session revocation for other devices.

8. Security considerations

9. Open questions

  1. Sliding vs fixed expiry. Current proposal is 12h rolling (extends on use). Alternative: 12h fixed to force daily re-assertion. Product call. Default here is rolling with 24h hard step-up ceiling.
  2. Refresh-token pattern for non-browser clients. Do we issue a separate refresh token to iOS/CLI, or require them to re-assert WebAuthn every 12h? Leaning toward "re-assert every 12h" for simplicity — iOS has good platform passkey support. Blocker for mobile roadmap, not for MBT v1.
  3. Admin session TTL. Console has 8h fixed per console.md. User sessions are 12h rolling. Keep divergent or align?

End of doc. Consumed by MBT (mbt-paper-trading-engine.md) and auth (auth.md).