AuthAuth Chain Reference

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

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:

PriorityMethodToken FormatProvider
1API Keyblq_... prefix (or legacy abc_...)Direct PostgreSQL lookup via Hyperdrive
2Better AuthCookie or bearer session IDBetterAuthProvider (primary)
3AnonymousNo credentialsFalls 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
}
TokenPatternRoute
blq_sk_live_xxxx...Starts with blq_→ API Key path
abc_sk_live_xxxx...Starts with abc_ (legacy)→ API Key path
sess_abc123xyzBetter 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:

  1. Cookie: better-auth.session_token=... — browser sessions
  2. Authorization: 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 MethodScope Check
better-authBypassed — session-authenticated users own the account
api-keyEnforced — scopes from the api_keys.scopes array
anonymousRejected — 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 users table
  • 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

ScenarioProviderWhy
New API integrationAPI Key (blq_ prefix)Scoped, revocable, rate-limited per key
Browser appBetter Auth (cookie)Server-side sessions, no JWTs in localStorage
Programmatic API callsBetter Auth (bearer plugin)Session-based auth without cookies
Public/unauthenticatedAnonymous10 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