Auth Chain Reference
Every request to the Cloudflare Worker is authenticated through a three-tier chain implemented in worker/middleware/auth.ts. The chain evaluates providers...
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
Sequence Diagram
Token Disambiguation
The auth middleware determines the token type by pattern matching:
// worker/middleware/auth.ts
/** 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:
// worker/middleware/auth.ts
// 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 scope
const scopeCheck = requireScope(context, 'compile', 'admin');
if (scopeCheck) return scopeCheck; // 403
Scope 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:
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 fallback
When 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 plugin
fetch('/api/compile', {
headers: {
'Authorization': `Bearer ${sessionToken}`,
},
});
// Or use an API key for server-to-server
fetch('/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