Next.js
Set up @better-iam/next in an App Router project, mount the IAM API, and read the session in server components, layouts, and client components.
@better-iam/next integrates Better IAM with the Next.js 15 App Router (React 19). The App Router spreads a
request across server components, layouts, route handlers, server actions, and middleware, and each exposes the
request differently: headers() and cookies() in components, a Request in route handlers, form data in
actions, and an edge runtime without database access in middleware. Calling the core API directly would mean
building the , redirecting with a safe ?next=, mapping refusals to status codes, and writing
cookies by hand in each of those places. One createIamNext(iam) call does it once and gives your server code:
- Sessions in server components:
getSession()andrequireSession(), memoized per request. - Guards:
page()for pages and layouts,route()andapiRoute()for route handlers,action()for server actions, andrequire()for one-off checks. - Rendering by permission:
can(),allowed(), and the<iamNext.Can>server component, batched into oneauthorizeManyper request. - A server-side typed client:
client()calls the IAM handler in process and writes the cookies it issues, so server actions can sign people in and out without client JavaScript. - Cross-site checks:
route(),apiRoute(), andpages.api()refuse cookie-authenticated mutations sent by another site's page (details). Next checks server actions itself. - Hydration:
sessionForClient()hands the session toIamNextProvider, so client components render the signed-in person on the first pass, matching the server's HTML (Client components). - Auth forms: drop-in server actions and unstyled, accessible forms for sign-in, MFA, step-up, password reset, sign-up, email verification, and invitations.
- Organizations in the URL, edge middleware, step-up, service credentials, webhooks and assertions, background delivery, and the Pages Router.
The package has three entries:
| Entry | Use from | Contents |
|---|---|---|
@better-iam/next | Server components, route handlers, server actions | createIamNext and everything in the edge entry |
@better-iam/next/edge | Middleware, edge route handlers, any Web runtime | createIamMiddleware, safeRedirectPath, assertion and webhook verification. No Node, React, or database imports, so the edge bundle stays small |
@better-iam/next/client | Client components ('use client') | IamNextProvider, useSignOut, the auth forms, and the React hooks |
The umbrella package exposes the same entries as better-iam/next, better-iam/next/edge, and
better-iam/next/client.
Install
npm i @better-iam/next @better-iam/client @better-iam/server @better-iam/adapter-sqliteOr install the umbrella package, better-iam, and import from its subpaths. See
Installation for other storage adapters.
Application layout
A complete App Router setup spreads over these files. The instance, the helpers, and the API route are the minimum; the others add edge redirects, drop-in forms, and background delivery.
| File | Holds | Runtime |
|---|---|---|
lib/iam.ts | betterIam({ ... }) | Node only (database, argon2) |
lib/iam-next.ts | createIamNext(iam, options) | Server components, actions, route handlers |
app/api/iam/[...path]/route.ts | iamNext.handlers() | The browser client, OAuth/SAML/SCIM mounts, /health |
middleware.ts | createIamMiddleware(...) | Edge: cookie presence and path forwarding |
app/auth-actions.ts | iamNext.authActions() | Sign-in, step-up, and reset server actions |
app/api/webhooks/iam/route.ts | createWebhookHandler(...) | Optional: consume your own IAM events |
app/api/cron/route.ts | iamNext.background.cron(...) | Optional: scheduled delivery and maintenance |
Setup
Create the instance
import { betterIam } from '@better-iam/server';
import { sqliteAdapter } from '@better-iam/adapter-sqlite';
export const iam = betterIam({
database: sqliteAdapter({ filename: process.env.BETTER_IAM_DATABASE ?? '.data/iam.db' }),
secret: process.env.BETTER_IAM_SECRET!,
baseURL: process.env.BETTER_IAM_BASE_URL ?? 'http://localhost:3000',
permissions: { actions: ['documents:read', 'documents:write'] },
});baseURL matters here: it becomes iam.endpoint.origin, the origin the in-process client presents and the one
the server trusts for cookie requests.
Create the Next.js helpers
createIamNext wraps the instance in the helpers your pages, route handlers, and actions use. loginPath and
stepUpPath are where guards send people who must sign in or confirm who they are.
import { createIamNext } from '@better-iam/next';
import { iam } from './iam';
export const iamNext = createIamNext(iam, {
loginPath: '/login',
stepUpPath: '/reauth',
interrupts: 'forbidden', // denials render app/forbidden.tsx
});Mount the IAM HTTP API
The browser client (including passkey ceremonies) and the OAuth, SAML, and SCIM protocol mounts reach the server through this catch-all route.
import { iamNext } from '@/lib/iam-next';
export const runtime = 'nodejs';
export const { GET, POST, OPTIONS } = iamNext.handlers();GET serves the operational /health and /metrics endpoints; every API method is a POST.
Keep native modules out of the bundle
The server and its native dependencies (argon2, better-sqlite3) must load as plain Node modules. List them in
serverExternalPackages, or load the instance lazily and pass a factory instead of the instance.
/** @type {import('next').NextConfig} */
export default {
serverExternalPackages: [
'@better-iam/server',
'@better-iam/auth',
'@better-iam/adapter-sqlite',
'argon2',
'better-sqlite3',
],
experimental: { authInterrupts: true }, // only for interrupts: true or 'forbidden'
};serverExternalPackages only applies to packages resolved from node_modules. In a monorepo, workspace
packages are symlinks outside it, so the example app and the console load the server with
import(/* webpackIgnore: true */ ...) instead.
Protect routes at the edge
Add middleware that redirects visitors without a session cookie and
forwards the requested path, so ?next= fills itself.
import { NextResponse } from 'next/server';
import { createIamMiddleware } from '@better-iam/next/edge';
export const middleware = createIamMiddleware({
loginPath: '/login',
publicPaths: ['/', '/pricing', '/docs/**', '/invite/*'],
next: (init) => NextResponse.next(init),
});
export const config = { matcher: ['/((?!_next|favicon.ico).*)'] };Reading the session
import { iamNext } from '@/lib/iam-next';
export default async function Dashboard() {
const current = await iamNext.getSession();
if (!current) return <a href="/login">Sign in</a>;
return <h1>Welcome, {current.identity.name}</h1>;
}iamNext.getSession() reads the request's cookies (through headers() and cookies()) and returns
{ identity, session }. It returns null whenever the server refuses the credential as a sign-in. That covers
missing, expired, or revoked sessions, sessions that must step up, unverified email addresses, inactive tenants,
and requests from a network the session or tenant does not allow. Any other failure is rethrown. requireSession() returns the session or redirects to the login
page with ?next=; Guards covers it with the other wrappers.
- Per-request memoization. The no-argument call goes through React
cache, so a layout, its page, and nested server components share one lookup per render. Calls that passheadersexplicitly are not memoized. Thecacheoption replaces React's; pass(fn) => fnto disable it. - After a server action. The helper merges
cookies()into the headers it sends. Cookies a server action just set, such as a new session after sign-in, therefore apply to the re-render Next performs in the same response. - Direct API calls.
iamNext.credential()returns{ headers }for anyiam.api.*call made as the current visitor:iam.api.identities.list(await iamNext.credential(), { tenantId }). - No cross-request state. The helpers never cache across requests. Memoization is scoped to one render, and there is no module-level session state.
Client components
Client components get the session from the server so their first render needs no fetch.
iamNext.sessionForClient() returns the session as plain JSON without the stored token hash, or null. Pass it
to IamNextProvider, which is IamProvider plus a router.refresh() whenever the signed-in identity changes in
the browser (details).
import type { ReactNode } from 'react';
import { iamNext } from '@/lib/iam-next';
import { Providers } from './providers';
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers initialSession={await iamNext.sessionForClient()}>{children}</Providers>
</body>
</html>
);
}Inside the provider, the React hooks (useSession, useAuthorize, useAccessible,
useIamClient, Can) work as usual; @better-iam/next/client re-exports them.
Options
createIamNext(iam, options) takes these options. Most applications set loginPath, stepUpPath, and
interrupts; headers, cookies, redirect, and the other overrides exist for tests and custom setups.
Prop
Type
createIamNext accepts the instance or a factory (() => iam or () => Promise<iam>). Any object with the
IamLike surface works; a custom one without endpoint passes baseURL and basePath.
iamNext at a glance
Everything createIamNext returns, and where each member is explained:
| Member | What it does | Details |
|---|---|---|
getSession(headers?) | The current { identity, session }, or null; memoized per request | Reading the session |
requireSession({ ... }) | The session, or a redirect to the login page (or the step-up page) | Guards |
require({ tenantId, action, resource }) | Enforces one action before rendering | Guards |
page(render, spec) | Wraps a page or layout: session, step-up, authorization, then render | Guards |
route(handler, spec) | Wraps a route handler for user sessions; answers IAM failures as JSON | Guards |
apiRoute(handler, spec) | route() that also accepts API keys and assumed roles | Service credentials |
action(fn, spec) | Wraps a server action; returns { ok, data } or { ok: false, error } | Server actions |
can({ tenantId, checks }) | Several advisory decisions in one call, keyed action@type/id | Rendering by permission |
allowed(action, resource?), <iamNext.Can> | One advisory decision, batched with every other check in the render | Rendering by permission |
client() | The typed API, running in process, writing issued cookies through cookies() | In-process client |
authActions(options) | Drop-in server actions for sign-in, step-up, reset, sign-up, verification, and invitations | Auth forms |
handlers() | GET, POST, and OPTIONS for the app/api/iam/[...path] route | Setup |
sessionForClient() | The session as plain JSON (no token hash) for IamNextProvider | Client components |
requireTenantSession({ slug }), tenant(slug) | Resolve an organization alias and require a session in it | Organizations |
assertion({ tenantId, audience }) | A short-lived signed assertion for a downstream service | Assertions |
credential() | { headers } for direct iam.api.* calls as the current visitor | Reading the session |
currentPath() | The path the middleware forwarded for this request, when present and safe | Middleware |
clearSessionCookie() | Expires the session cookie in the current response | Server actions |
background | Outbox and event dispatch: dispatch(), schedule(), and the cron() route | Background work |
pages | Pages Router helpers: handler, withSession, api, client, getSession | Pages Router |
In this section
Guards
page, requireSession, require, route, and rendering by permission.
Server actions and forms
The in-process client, guarded actions, and the drop-in auth forms.
Organizations in the URL
requireTenantSession for /[org]/... routes.
Middleware
Edge redirects, public paths, and ?next= forwarding.
Advanced
Step-up, service credentials, webhooks, assertions, background work, and the Pages Router.
The example app (examples/nextjs) runs
all of it on port 3300 with seeded accounts.
Was this page helpful?
Last updated on