401 — every route except GET /health requires an authenticated user.
Pattern 1 — resolveUserFrom (the Next.js-native choice)
It receives the Web Request the route handler was called with. In practice most Next auth
libraries read the session from async context rather than from the request object, so the resolver
usually ignores its argument:
app/api/persona/[...persona]/route.ts
null and thrown errors are equivalent — both produce 401 — so a
session lookup that throws is treated as “not authenticated”, never as a 500.
NextAuth / Auth.js
Supabase
Your own session cookie
Pattern 2 — Runtime-level resolver (resolveUser)
The general resolver, shared with every other Persona adapter. It receives the translated
request (the runtime’s RuntimeRequest shape), so you read request.headers, request.query,
etc. Use it when your auth already works off raw headers, or when you want the same runtime
mountable through more than one transport:
Which one?One of the two is required —resolveUserFromis the ergonomic choice in Next.js — it runs in the route handler’s own async context, which is exactly whatauth(),cookies(), andheaders()need.resolveUseris the portable choice. If you provide both,resolveUserFromwins (it is applied last, and the runtime-level resolver becomes a pass-through of the identity it resolved).
createPersonaHandler throws at module load if neither is given, so
a misconfiguration fails your build, not a user’s request.
Middleware still runs first
Next.jsmiddleware.ts runs before route handlers, so a matcher that covers /api/persona/:path*
gates the mount the same way an Express auth middleware would:
middleware.ts
auth() to see a session inside the handler at all.
Unauthenticated requests
When the resolver returnsnull (or throws), the runtime responds 401 with the envelope:
middleware.ts — the adapter itself never redirects; it only ever lets
the runtime answer.
Using your own identity vs forwarding client input
The adapter never passes through raw identity from the browser. The resolver reads whatever your auth verified — Clerk’sauth().userId, an Auth.js session, your own signed cookie — and that
verified id is what Persona scopes the request to. Never derive it from an untrusted header
the client could forge; always go through a step your backend actually authenticated.
Health is public
GET /health does not call the resolver at all — it’s a liveness/capability probe backed by the
SDK’s whoami(). It’s the one route that answers without an authenticated user. See
Runtime routes for the full always-on table.