Toby Allen

Deploying @auth0/auth0-hono to Cloudflare Workers: Three Things the Guide Doesn't Mention

· Auth0, Customer Identity Products, Identity Security

Auth0 published a guide for adding authentication to a Hono application on Cloudflare Workers using @auth0/auth0-hono, an official middleware SDK that wraps the OIDC flow, session handling, login/callback/logout routing, and access token refresh behind a single auth0() call. I wanted to compare it directly against the Traditional and Backend-for-Frontend demos already in my Auth0 session and token demo suite, both of which use @auth0/nextjs-auth0 on Vercel's Node.js runtime, so I built the guide's example for real, deployed it to Cloudflare, and put a live comparison page next to the other two.

The comparison itself turned out to be straightforward once it was running. Getting it running was not. In this post I will detail three things the guide does not mention that I hit deploying the real thing, and what actually changes architecturally once you move a confidential Auth0 client from Node.js serverless to Cloudflare's edge runtime.

The steps we will cover in this post are as follows.

  1. authRequired defaults to true and protects everything
  2. An undocumented Node API dependency breaks the build
  3. What genuinely changes vs a Node.js confidential client

authRequired defaults to true and protects everything

The guide's minimal setup is app.use('*', auth0()), and I wired mine up exactly the same way. My home route has no requiresAuth() on it — it just renders a login link if you're signed out. It came back 401 Authentication required on every request regardless.

Reading the SDK's own source rather than the README explained it: authRequired defaults to true, and when it is true, auth() runs every non-login/callback/logout request through the same check requiresAuth() does. This is the same default express-openid-connect — Auth0's older Express equivalent — has always shipped with, so it is not a new decision, but it is not called out in the Hono guide either, and the failure mode is a flat 401 with no obvious cause unless you already know to look for it. The fix is one line:

app.use('*', auth0({ authRequired: false }))

An undocumented Node API dependency breaks the build

wrangler deploy --dry-run failed on the very first attempt:

✘ [ERROR] Could not resolve "async_hooks"

    node_modules/@auth0/auth0-hono/dist/session/HonoCookieHandler.js:1:34:
      1 │ import { AsyncLocalStorage } from "async_hooks";
        ╵                                   ~~~~~~~~~~~~~

  The package "async_hooks" wasn't found on the file system but is built into node.
  - Add the "nodejs_compat" compatibility flag to your project.

The SDK's cookie handler imports AsyncLocalStorage from Node's async_hooks module internally. Cloudflare Workers can shim enough of this for the SDK to work — but only if you ask it to. Add the flag to wrangler.jsonc and the build goes through clean:

{
  "compatibility_flags": ["nodejs_compat"]
}

The guide lists Cloudflare Workers as a first-class target for this SDK, and it is, but nothing in the setup instructions mentions that a Node compatibility flag is a prerequisite. I found this by reading a failed build log, not a docs page.

What genuinely changes vs a Node.js confidential client

Once the app was actually running, the comparison against my Traditional and BFF demos came down to two axes, not the flow itself.

The OIDC flow is identical. Same Authorization Code exchange, same confidential client with a client secret, same claim set. What differs is where the session lives and what runtime issues the request.

Session storage. My Traditional and BFF demos back @auth0/nextjs-auth0's session store with Firestore — the cookie carries nothing but an opaque, randomly generated session ID, and the actual session document lives server-side where it can be looked up, inspected, and deleted. @auth0/auth0-hono's default mode is stateless: the session data itself lives inside an encrypted cookie, and there is no database at all. That is a genuine simplification — nothing to provision, nothing to keep available — and it comes with a real cost. Revoking a session in my Traditional demo means deleting a Firestore document, and Back-Channel Logout already does this automatically when a session ends elsewhere. In the default Hono/Workers mode there is no document to delete. A stolen session cookie stays valid until it expires or you rotate the signing key tenant-wide — there is no equivalent kill switch for one session. The SDK does ship a KVSessionStore for Cloudflare KV that restores server-side state and Back-Channel Logout support, but it is an opt-in, not the path this specific comparison exercises. There's a second, more mundane limit worth flagging too: packing an ID token, access token, refresh token, and profile claims into one encrypted cookie can approach the ~4KB per-cookie limit browsers enforce faster than it looks in a demo with a minimal user — a tenant with several custom claims or a larger profile could hit it for real. The same KVSessionStore fix applies here as well.

Runtime. Vercel Functions run Node.js in a serverless container; Cloudflare Workers run in a V8 isolate distributed globally at the edge. For a login flow this mostly shows up as different cold-start and latency characteristics rather than anything visible in the token exchange itself, but it is the reason the async_hooks gotcha above exists at all — a V8 isolate is not a Node process, and anything reaching for a genuine Node built-in needs the compatibility flag to pretend otherwise.

Neither of these makes one approach better than the other. A stateless cookie session is a legitimate choice when you don't want to run a database at all, in the same way a Firestore-backed session is the right choice when you need Back-Channel Logout to actually revoke something. They are different trade-offs for the same problem, not a spectrum with a correct end.

Conclusion

The comparison page in my demo suite links out to the real deployed Worker rather than embedding it, and puts the session-storage and runtime differences side by side against Traditional and BFF directly. If you are following Auth0's own guide to build this yourself, authRequired: false and the nodejs_compat flag will save you the exact debugging session I had.