Quickback Docs

JWT Optimization

Zero-query authentication using HMAC-signed JWTs. Eliminates database lookups for authenticated API requests.

Quickback automatically optimizes authenticated API requests using JWT tokens. After the first request (which uses session cookies), subsequent requests skip the database entirely by sending a signed JWT that encodes the full auth context.

How It Works

Request Flow

First request (no JWT cached):
  Browser → Cookie auth → 2 DB queries → Response + set-auth-token header
  Browser caches JWT in localStorage

Subsequent requests (JWT cached):
  Browser → Bearer JWT → Signature check only → Response (0 DB queries)

What's in the JWT

The JWT encodes everything needed for auth middleware:

ClaimDescription
subUser ID
orgIdActive organization ID
roleOrganization membership role (owner, admin, member)
userRoleGlobal user role (when admin panel is enabled)
emailUser email
nameUser display name
iatIssued-at timestamp
expExpiry (15 minutes from issue)

Signing

JWTs are signed with HMAC-SHA256 using your BETTER_AUTH_SECRET. No additional configuration is needed — this uses the same secret you already set for Better Auth.


Token Lifecycle

Automatic Minting

When a request arrives without a JWT (or with an expired one), the auth middleware:

  1. Authenticates via session cookie (existing Better Auth flow)
  2. Signs a JWT with the full auth context
  3. Returns it in the set-auth-token response header

The Quickback API client (quickback-client.ts) automatically captures this header and stores the JWT in localStorage.

Re-minting on Org Switch

When a user switches organizations, the JWT must be re-minted because orgId and role change. The auth UI handles this automatically:

// This happens automatically in the org switcher
await authClient.organization.setActive({ organizationSlug: slug });

// Fetch fresh JWT with new org context
const res = await fetch('/api/v1/token', {
  method: 'POST',
  credentials: 'include',
});
const { token } = await res.json();
localStorage.setItem('bearer_token', token);

Invalidation on Role Changes

When an admin changes a user's role or removes them from an organization, the server broadcasts an auth:token-invalidated event via WebSocket. The client automatically clears the cached JWT, forcing the next request to fall back to session auth and mint a fresh token.

Admin changes user role
  → databaseHooks fires on member update
  → Broadcasts auth:token-invalidated via realtime
  → Client clears localStorage JWT
  → Next request uses session cookie → gets fresh JWT

Expiry

JWTs expire after 15 minutes. This is a safety net — in practice, JWTs are re-minted well before expiry through the set-auth-token response header on session-fallback requests.

When a JWT expires or is invalid:

  1. The middleware silently falls back to session cookie auth
  2. A fresh JWT is minted and returned in the response header
  3. The client stores the new JWT for subsequent requests

Token Endpoint

POST /api/v1/token mints a fresh JWT from the current session. This endpoint goes through normal auth middleware, so the user must have a valid session cookie.

Request:

curl -X POST https://your-api.example.com/api/v1/token \
  -H "Content-Type: application/json" \
  --cookie "better-auth.session_token=..."

Response:

{
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}

Use this endpoint after operations that change auth context (like switching organizations) to get a JWT that reflects the new state.


Client Integration

Quickback API Client

The quickback-client.ts API client handles JWT auth automatically:

  • Sends the cached JWT as Authorization: Bearer <token> on every request
  • Captures refreshed JWTs from set-auth-token response headers
  • Clears the JWT on 401 responses (falls back to session cookie)

No additional client configuration is needed.

Custom API Calls

If you make direct fetch() calls to your API (outside the Quickback client), include the JWT:

const jwt = localStorage.getItem('bearer_token');

const res = await fetch('https://your-api.example.com/api/v1/things', {
  headers: {
    'Authorization': `Bearer ${jwt}`,
    'Content-Type': 'application/json',
  },
  credentials: 'include', // Fallback to session cookie if no JWT
});

// Capture refreshed token
const newToken = res.headers.get('set-auth-token');
if (newToken) {
  localStorage.setItem('bearer_token', newToken);
}

Sign Out

Clear the JWT on sign-out to prevent stale tokens:

localStorage.removeItem('bearer_token');
await authClient.signOut();

Security

Threat Model

ThreatMitigation
Token theft (XSS)15-minute expiry limits exposure window. httpOnly cookies protect the session.
Token replayShort expiry + set-auth-token rotation on session fallback
Stale permissionsWebSocket auth:token-invalidated broadcast on role changes
Token forgeryHMAC-SHA256 signature verified with BETTER_AUTH_SECRET
Timing attackscrypto.subtle.verify() provides constant-time comparison

Key Points

  • JWTs are a performance optimization, not a replacement for session auth
  • Session cookies remain the source of truth — JWTs are derived from them
  • If a JWT is lost, stolen, or cleared, the system gracefully falls back to cookie auth
  • No additional secrets or configuration needed — uses your existing BETTER_AUTH_SECRET
  • The JWT contains no sensitive data beyond what's already in the auth context

Files Worker

The files worker (for Cloudflare R2 file uploads) also supports JWT authentication. When a request includes a JWT Authorization header, the worker verifies it directly instead of calling back to the auth API — eliminating a network round-trip for file operations.


On this page