Set up Auth0
Wire the drop-in Auth0 toggle the same way the reference deployment does: a
non-prefixed server flag as the single source of truth, secrets that never
get a NEXT_PUBLIC_ twin, a client provider that only mounts when the flag
says so, and per-page server-side enforcement. Background and the full file
listing live in ../../docs/11-auth0-recipe.md
— this file is the procedure, not the rationale. Step 1 also leans on
../../docs/01-runtime-env-injection.md
for the general build-time-vs-runtime env split this toggle reuses.
When to use this
You're adding Auth0 login to a deployment that currently runs without it, or you're debugging a session that isn't behaving — a user who can't stay logged in, a page that isn't actually protected, or a crash right after a secret rotation. If auth is already wired and you're just protecting one more page, skip to step 5.
Procedure
Set the env vars. File:
.env.example(and the deploy-time entrypoint script that regeneratespublic/env.js— see../../docs/01-runtime-env-injection.mdfor that mechanism). AddAUTH0_ENABLED,AUTH0_SECRET,APP_BASE_URL,AUTH0_DOMAIN,AUTH0_ISSUER_BASE_URL,AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRET— all runtime, server-only, non-NEXT_PUBLIC_— plus the client-visibleNEXT_PUBLIC_AUTH0_ENABLEDandNEXT_PUBLIC_PROFILE_ROUTE. Every var that carries a secret (AUTH0_SECRET,AUTH0_CLIENT_SECRET) must never grow aNEXT_PUBLIC_twin — if a later change seems to need one client-side, that's a sign to route the value through a server action instead, not to add the twin.Instantiate the
Auth0Clientand wire the auth route. Files:src/lib/auth0.ts(new),app/api/auth/[auth0]/route.ts(new). Insrc/lib/auth0.ts, constructnew Auth0Client({ routes: { login, callback, logout }, session: { absoluteDuration } })and export it alongside agetSessionSafely()wrapper (see step 6's debug checklist for why that wrapper exists). In the route file, export aGEThandler that does nothing butreturn auth0.middleware(request)— every Auth0 flow (login,callback,logout,me) is delegated to the SDK, no per-route logic to write. Full listings in doc 11, step 2.Add the conditional
AuthProvider. File:src/components/providers/AuthProvider.tsx(new). A'use client'component that readsNEXT_PUBLIC_AUTH0_ENABLEDvia the sharedgetRuntimeEnv()helper (neverwindow.__ENV__orprocess.envdirectly) and renders<Auth0Provider>{children}</Auth0Provider>when it's'true', or just{children}otherwise — so the Auth0 client SDK never even mounts when auth is off. Place it as the innermost provider in the root layout's provider stack (see../../docs/10-providers.mdfor provider ordering).Sync the public flag from the server flag in the entrypoint script. File:
startup.sh(production entrypoint) — and mirror it in the local dev generator (generate-env.mjsor equivalent) so the two never drift. Before regeneratingpublic/env.js, exportNEXT_PUBLIC_AUTH0_ENABLED=${AUTH0_ENABLED}so the client-visible flag is always derived from the server flag, never set independently. A generator that only mirrors whateverNEXT_PUBLIC_AUTH0_ENABLEDalready happens to be is a weaker version of this and is fine for a laptop, but is not the pattern to copy for the deploy path.Protect a page via server-side session check. File: the page's
page.tsx(e.g.app/dashboard/page.tsx). Addsrc/utils/auth-config.server.tsonce, exportingisAuthEnabled()(readsAUTH0_ENABLEDfresh fromprocess.env, falling back toNEXT_PUBLIC_AUTH0_ENABLEDonly if the server var was never set — see doc 11 step 4 for the exact body). Then in the page component:export default async function DashboardPage() { if (isAuthEnabled()) { const session = await getSessionSafely() if (!session?.user) redirect('/') } return <DashboardClient /> }This
ifblock, repeated per protected page, is the entire enforcement story — see the debug checklist below for why it can't live in the proxy instead.Debug checklist. Work top to bottom; each row is symptom → likely cause → fix.
Symptom Likely cause Fix User is logged out right after a deploy that rotated AUTH0_SECRET, orgetSession()throws instead of returningnullOld session cookie was encrypted under the previous AUTH0_SECRET; the SDK throwsERR_JWE_DECRYPTION_FAILEDwhen it can't decrypt it under the new oneCall through getSessionSafely()(step 2), which catches exactly that error code and returnsnull— treat it as "no session," not a 500. Re-throw every other error code; those are real misconfiguration (badAUTH0_DOMAIN, network failure) and should surface.useUser()(or any Auth0 hook) throws, or React warns about hooks changing order between renders, after adding an enable/disable branch near a component that uses itA hook-after-conditional bug: an early return ( if (!authEnabled) return null) was placed above the hook call, so the hook runs on some renders and not othersCall every hook unconditionally, before any early return. Do the enable/disable branching after all hooks have run inside the component, or move the branch to the parent that decides whether to render the component at all. A route added to the proxy's matcher is reachable by an unauthenticated user even though AUTH0_ENABLED=trueThe proxy is permissive by design — it checks isAuthEnabled()but still calls through for every matched route; it never redirects or blocks, because parsing the session cookie at the proxy layer is fragile across SDK versionsDon't rely on the proxy matcher for protection. Add the server-side check from step 5 ( isAuthEnabled()+getSessionSafely(), oruseUser()client-side) directly in that page's own component.Auth behaves differently than expected in one environment but not another, or toggling AUTH0_ENABLEDin one place doesn't seem to change client behaviorNEXT_PUBLIC_AUTH0_ENABLEDdrifted fromAUTH0_ENABLEDbecause the entrypoint script wasn't updated to derive one from the otherConfirm startup.sh(or the dev generator) exportsNEXT_PUBLIC_AUTH0_ENABLED=${AUTH0_ENABLED}before regeneratingenv.js(step 4) — the public flag must always be derived, never set independently.A secret ( AUTH0_CLIENT_SECRET,AUTH0_SECRET) shows up in a client bundle or browser network tabSomething added a NEXT_PUBLIC_twin of a secret var, or read the secret from a client componentRemove the twin. Route the value through a server action or an API route instead — see step 1.
Rules carried over from the reference doc
AUTH0_ENABLED(server, non-prefixed) is the single source of truth;NEXT_PUBLIC_AUTH0_ENABLEDis a client-side rendering hint only, never the primary enforcement check.- Enforcement lives in page components, not the proxy — every protected page
is individually responsible for calling
getSessionSafely()oruseUser()and redirecting itself. - No secret-bearing var ever grows a
NEXT_PUBLIC_twin. getSessionSafely()swallows exactly one error code (ERR_JWE_DECRYPTION_FAILED) and re-throws everything else.