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.


Server-Side Validation

Every authenticated API request is validated server-side before it reaches your handler. There is no "trust the client" shortcut — the middleware runs full signature verification on each request.

Validation steps (in generated src/middleware/auth.ts):

  1. Extract the Authorization: Bearer <token> header
  2. Call verifyJwt(token, BETTER_AUTH_SECRET) from src/lib/jwt.ts
  3. Decode header + payload, recompute the HMAC-SHA256 signature, and compare via crypto.subtle.verify() (constant-time — no timing-attack surface)
  4. Check exp against the current time
  5. On success: hydrate AppContext from claims (sub, orgId, role, userRole, email, name) — no DB query
  6. On failure (bad signature, expired, malformed): silently fall back to Better Auth's session cookie auth; if that succeeds, mint a fresh JWT and return it in the set-auth-token response header

The Files worker (R2 uploads) performs the same verification using the shared secret — no network round-trip to the auth API for file operations.


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.


Relation to Better Auth's JWT Plugin

Quickback's JWT fast-path (documented above) is a custom HMAC-SHA256 implementation, separate from Better Auth's jwt plugin. The distinction matters if you're comparing options.

What Quickback ships by default

  • Custom HMAC-SHA256 JWTs minted by the compiled API at POST /api/v1/token
  • Payload includes sub, orgId, role, userRole, email, name — everything the middleware needs to reconstruct AppContext with zero DB queries
  • Signed with BETTER_AUTH_SECRET
  • Used by the generated auth middleware as a fast-path ahead of getSession()

What Better Auth's JWT plugin gives you (opt-in)

Enable it with plugins: ["jwt"] in your auth config:

auth: defineAuth("better-auth", {
  plugins: ["jwt"],
}),

You get:

  • JWKS endpoint at /auth/v1/jwks for asymmetric public-key verification by external services
  • BA's own /auth/v1/token endpoint (asymmetric-signed, EdDSA by default)
  • Standard OIDC-style claims via BA's definePayload hook

Why Quickback doesn't use BA's plugin internally

Better Auth's definePayload hook only receives { user }. It cannot embed the active organization context (orgId, org-member role) that Quickback's Firewall and Access layers need. Without that context in the JWT, the middleware would have to do a DB lookup per request — defeating the whole point of the fast-path.

When to enable BA's JWT plugin anyway

Turn it on if external services need to verify tokens without sharing BETTER_AUTH_SECRET — e.g. a third-party worker that wants to confirm a user's identity via JWKS. The BA-issued tokens won't have Quickback's org context, so they're useful for identity assertions, not for hitting the Quickback API directly.

Enabling BA's plugin does not change how the compiled API authenticates requests — the middleware keeps using the custom HMAC JWT regardless.


On this page