A working sign-in flow with the token held server-side in an HttpOnly cookie.
The example depends on the library by path (file:../..), so build the library first.
# from the repository root
pnpm install
pnpm build
cd examples/next-js
cp .env.example .env.local # then set AUTH_SECRET
pnpm install
pnpm devOpen http://localhost:3000 and sign in with ada@example.com / password.
| File | Shows |
|---|---|
src/auth.ts |
Server config — authenticate, refresh, and the Next adapter |
src/auth-client.ts |
createAuthClient<User>(), typed once |
src/app/api/auth/[...auth]/route.ts |
All five routes in one line |
src/app/layout.tsx |
Server-resolved initialSession — no loading flash |
src/middleware.ts |
The real access control |
src/app/dashboard/page.tsx |
Server component reading the access token |
src/app/login/page.tsx |
signIn({ email, password }) — credentials, not a token |
Sign in, then open devtools and run:
document.cookie;You will see csrf=… and not the session cookie. The credential is HttpOnly, so no script on the page — yours or a compromised dependency's — can read or exfiltrate it.
Compare the two halves of the dashboard: the server component prints the access token because it can, while the browser only ever received user and expiresAt.
Both were caught by the e2e suite, and both fail silently — the app still redirects, just from the page instead of the server, so nothing looks broken:
middleware.tsmust live insrc/when the project uses asrc/directory. At the project root it is never compiled, and the middleware simply does not run.matcher: ['/dashboard/:path*']does not match/dashboarditself — only its children. Both entries are needed.
src/auth.ts fakes an identity provider so the example runs with no external services. Replace authenticate and refresh with real calls; nothing else changes.
Middleware is the enforcement point. useRequireAuth, SignedIn and SignedOut keep the UI coherent but run in the browser after render — they are not access control.