Skip to content

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

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

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:

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:

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 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