Skip to main content
Persona never authenticates end users. There is no login, no session, no token of its own — the route handler receives a request and asks your code who it’s from. The adapter gives you two ways to answer. The result of either resolver is the external user id Persona scopes the request to: threads, files, and memory are that user’s, and chat runs under that identity. If no user can be resolved, the runtime responds 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
The resolver may be async. 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

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? resolveUserFrom is the ergonomic choice in Next.js — it runs in the route handler’s own async context, which is exactly what auth(), cookies(), and headers() need. resolveUser is the portable choice. If you provide both, resolveUserFrom wins (it is applied last, and the runtime-level resolver becomes a pass-through of the identity it resolved).
One of the two is required — 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.js middleware.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
This is complementary, not a replacement: middleware decides whether the request may proceed; the resolver tells Persona whose request it is. Most Clerk and Auth.js setups need the matcher to include your Persona route for auth() to see a session inside the handler at all.

Unauthenticated requests

When the resolver returns null (or throws), the runtime responds 401 with the envelope:
If you’d rather reject unauthenticated requests yourself (redirect to a login page, return your own error page), do it in 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’s auth().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.