Auth Chain Reference
Auth Chain Reference
Runtime authentication flow — How the three-tier auth chain works, feature flags, and the completed Clerk → Better Auth migration.
Table of Contents
- Overview
- Auth Chain Priority
- Sequence Diagram
- Token Disambiguation
- Better Auth (Primary Provider)
- Auth Guards
- When to Use Which Provider
Overview
Every request to the Cloudflare Worker is authenticated through a three-tier chain
implemented in worker/middleware/auth.ts. The chain evaluates providers in strict
priority order and short-circuits on the first successful match:
| Priority | Method | Token Format | Provider |
|---|---|---|---|
| 1 | API Key | blq_... prefix (or legacy abc_...) | Direct PostgreSQL lookup via Hyperdrive |
| 2 | Better Auth | Cookie or bearer session ID | BetterAuthProvider (primary) |
| 3 | Anonymous | No credentials | Falls through to anonymous context |
The chain never throws — all failures are communicated via the response field
on IAuthMiddlewareResult.
Auth Chain Priority
flowchart TD
REQ["Incoming Request"] --> TOKEN{"Extract Bearer<br/>token"}
TOKEN -->|"blq_/abc_ prefix"| APIKEY["API Key Path"]
TOKEN -->|"No token or<br/>non-API-key token"| BA["Better Auth<br/>(Primary Provider)"]
APIKEY --> HASH["SHA-256 hash token"]
HASH --> LOOKUP["Query api_keys table<br/>via Hyperdrive"]
LOOKUP -->|"Found + valid"| TIER["Resolve owner tier<br/>from users table"]
TIER --> AUTH_OK["Authenticated<br/>(api-key method)"]
LOOKUP -->|"Not found / expired / revoked"| REJECT["401 Rejected"]
BA -->|"Valid session<br/>(cookie or bearer)"| ZTA["Run Token Validators<br/>(ZTA checks)"]
ZTA -->|"Pass"| BA_OK["Authenticated<br/>(better-auth method)"]
ZTA -->|"Fail"| REJECT2["401 Rejected"]
BA -->|"No credentials<br/>(no error)"| CLERK_CHECK{"Clerk fallback<br/>enabled?"}
BA -->|"Error<br/>(bad token)"| REJECT3["401 Rejected"]
CLERK_CHECK -->|"Yes + JWT token"| CLERK["Clerk JWT<br/>(Fallback Provider)"]
CLERK_CHECK -->|"No or not JWT"| ANON["Anonymous<br/>(10 req/min)"]
CLERK -->|"Valid JWT"| CLERK_OK["Authenticated<br/>(clerk-jwt method)<br/>Deprecation warning logged"]
CLERK -->|"Invalid JWT"| REJECT4["401 Rejected"]
CLERK -->|"No credentials"| ANON
style REQ fill:#37474f,stroke:#263238,color:#fff
style AUTH_OK fill:#1b5e20,stroke:#0a3010,color:#fff
style BA_OK fill:#1b5e20,stroke:#0a3010,color:#fff
style CLERK_OK fill:#b84000,stroke:#7a2900,color:#fff
style ANON fill:#37474f,stroke:#263238,color:#fff
style REJECT fill:#c62828,stroke:#8e1c1c,color:#fff
style REJECT2 fill:#c62828,stroke:#8e1c1c,color:#fff
style REJECT3 fill:#c62828,stroke:#8e1c1c,color:#fff
style REJECT4 fill:#c62828,stroke:#8e1c1c,color:#fff
Sequence Diagram
sequenceDiagram
participant C as Client
participant W as Worker (Hono)
participant AM as authenticateRequestUnified()
participant BA as BetterAuthProvider
participant HD as Hyperdrive
participant DB as Neon PostgreSQL
C->>W: POST /api/compile (Bearer blq_xxx)
W->>AM: authenticate(request, env)
AM->>AM: extractBearerToken() → "blq_xxx"
AM->>AM: isApiKeyToken("blq_xxx") → true
AM->>AM: hashToken("blq_xxx") → SHA-256
AM->>HD: query api_keys WHERE key_hash = $1
HD->>DB: SQL query
DB-->>HD: row { id, user_id, scopes }
HD-->>AM: result
AM->>HD: query users WHERE id = $1 (resolve tier)
HD->>DB: SQL query
DB-->>HD: row { tier: "pro" }
AM-->>W: { context: { authMethod: "api-key", tier: "pro" } }
W-->>C: 200 OK (compiled output)
Note over C,W: --- Different client, Better Auth session ---
C->>W: GET /api/users/me (Cookie: better-auth.session=xxx)
W->>AM: authenticate(request, env)
AM->>AM: extractBearerToken() → null (cookie-based)
AM->>BA: verifyToken(request)
BA->>BA: auth.api.getSession({ headers })
BA-->>AM: { valid: true, providerUserId: "user-uuid" }
AM->>AM: runTokenValidators() → { valid: true }
AM-->>W: { context: { authMethod: "better-auth", tier: "free" } }
W-->>C: 200 OK (user profile)
Token Disambiguation
The auth middleware determines the token type by pattern matching:
/** API keys start with the "blq_" prefix (current) or "abc_" prefix (legacy) */function isApiKeyToken(token: string): boolean { return isApiKey(token); // delegates to api-key-utils.ts}| Token | Pattern | Route |
|---|---|---|
blq_sk_live_xxxx... | Starts with blq_ | → API Key path |
abc_sk_live_xxxx... | Starts with abc_ (legacy) | → API Key path |
sess_abc123xyz | Better Auth session token | → Better Auth (via cookie or bearer plugin) |
| (none) | No Authorization header | → Better Auth (checks cookies) → Anonymous |
Better Auth (Primary Provider)
Implementation: worker/middleware/better-auth-provider.ts
Better Auth is always the first provider consulted for non-API-key requests.
It handles both cookie-based browser sessions and bearer-token API sessions
(via the bearer() plugin).
// worker/lib/auth.ts — createAuth()
export function createAuth(env: Env, baseURL?: string) { const prisma = createPrismaClient(env.HYPERDRIVE!.connectionString);
return betterAuth({ database: prismaAdapter(prisma, { provider: 'postgresql' }), secret: env.BETTER_AUTH_SECRET!, basePath: '/api/auth', baseURL, emailAndPassword: { enabled: true }, user: { additionalFields: { tier: { type: 'string', required: false, defaultValue: 'free', input: false }, role: { type: 'string', required: false, defaultValue: 'user', input: false }, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // refresh within 1 day of expiry }, plugins: [bearer()], });}Session Resolution
Better Auth resolves sessions using auth.api.getSession(), which checks:
Cookie: better-auth.session_token=...— browser sessionsAuthorization: Bearer <session-id>— API sessions (bearer plugin)
Tier Resolution (ZTA)
Tier and role are read from the database on every request — not cached in the
session token. This enables Zero Trust Architecture: revoking a user’s admin role
takes effect immediately without waiting for token expiry.
Auth Guards
The auth middleware provides helper functions for route-level access control:
// Require any authenticated user (rejects anonymous)const authCheck = requireAuth(context);if (authCheck) return authCheck; // 401
// Require minimum tier (e.g., Pro)const tierCheck = requireTier(context, UserTier.Pro);if (tierCheck) return tierCheck; // 403
// Require specific API key scopeconst scopeCheck = requireScope(context, 'compile', 'admin');if (scopeCheck) return scopeCheck; // 403Scope Bypass Rules
| Auth Method | Scope Check |
|---|---|
better-auth | Bypassed — session-authenticated users own the account |
api-key | Enforced — scopes from the api_keys.scopes array |
anonymous | Rejected — anonymous users have no scopes |
Migration Timeline
The Clerk → Better Auth migration follows a phased approach:
gantt
title Clerk → Better Auth Migration
dateFormat YYYY-MM-DD
axisFormat %Y-%m-%d
section Phase 1: Foundation
Prisma adapter + Better Auth setup :done, p1a, 2025-03-01, 14d
Auth chain with dual providers :done, p1b, after p1a, 7d
Feature flags added :done, p1c, after p1b, 3d
section Phase 2: Client Migration
Frontend Better Auth forms :active, p2a, 2025-03-25, 14d
CLI auth migration :p2b, after p2a, 7d
Monitor Clerk fallback usage :p2c, after p2a, 21d
section Phase 3: Clerk Removal
Set DISABLE_CLERK_FALLBACK=true :p3a, after p2c, 3d
Set DISABLE_CLERK_WEBHOOKS=true :p3b, after p3a, 3d
Remove Clerk code + dependencies :p3c, after p3b, 7d
Delete Clerk project :p3d, after p3c, 3d
Current State
- Better Auth is the primary provider and handles all new sign-ups
- Clerk fallback remains enabled for clients not yet migrated
- Clerk webhooks still sync user data to the PostgreSQL
userstable - The deprecation warning in logs tracks how many requests still use Clerk
Monitoring Fallback Usage
To check whether any clients still use Clerk, search Worker logs for:
[auth] Request authenticated via DEPRECATED Clerk fallbackWhen this message stops appearing, it’s safe to proceed to Phase 3.
When to Use Which Provider
| Scenario | Provider | Why |
|---|---|---|
| New API integration | API Key (blq_ prefix) | Scoped, revocable, rate-limited per key |
| Browser app | Better Auth (cookie) | Server-side sessions, no JWTs in localStorage |
| Programmatic API calls | Better Auth (bearer plugin) | Session-based auth without cookies |
| Public/unauthenticated | Anonymous | 10 req/min rate limit, basic features only |
Recommendation
For all new development, use Better Auth:
// Browser: Better Auth handles cookies automatically via /api/auth/*// See: worker/lib/auth.ts → basePath: '/api/auth'
// API: Use the bearer pluginfetch('/api/compile', { headers: { 'Authorization': `Bearer ${sessionToken}`, },});
// Or use an API key for server-to-serverfetch('/api/compile', { headers: { 'Authorization': `Bearer blq_sk_live_...`, },});Further Reading
- Better Auth User Guide — End-user sign-up, sign-in, and account management
- Better Auth Developer Guide — Integration reference for backend and frontend
- Better Auth + Prisma — Prisma adapter configuration
- API Authentication — API key creation and management
- Clerk → Better Auth Migration Guide — Historical reference for the completed migration
- Developer Guide — Full auth integration tutorial