BetterIAM
Next.js

Middleware

Redirect signed-out visitors at the edge with createIamMiddleware, keep public paths open, and forward the requested path so ?next= fills itself.

createIamMiddleware from @better-iam/next/edge sends visitors without a cookie to the login page before a protected page starts rendering, and forwards the requested path to your server components. It is a routing convenience, not authorization.

Why use it: a signed-out visitor is redirected at the edge, before any server component runs or queries anything. Also, App Router server components are not given the full path of the request they render. Without the forwarded path, a guard deep in a layout would not know where to send the person back after sign-in.

Middleware checks presence, not validity

Middleware runs on the edge and cannot open the database. It only checks that the session cookie is present: __Host-better-iam.session on HTTPS and better-iam.session on loopback HTTP. Pages must still authenticate with requireSession, page, or requireTenantSession. A stale cookie gets through the middleware, and the page's guard then redirects.

Setup

middleware.ts
import { NextResponse } from 'next/server';
import { createIamMiddleware } from '@better-iam/next/edge';

export const middleware = createIamMiddleware({
  loginPath: '/login',
  publicPaths: ['/', '/pricing', '/docs/**', '/invite/*'],
  signedInRedirect: '/dashboard',
  next: (init) => NextResponse.next(init),
});
export const config = { matcher: ['/((?!_next|favicon.ico).*)'] };

Import middleware helpers from @better-iam/next/edge. That entry has no Node, React, or database imports, so the edge bundle stays small. The main entry re-exports everything in it.

Prop

Type

What it does, per request

  • Public paths. The default rule protects everything except the login path (and paths below it), anything under /api/iam, and the publicPaths globs. publicPaths adds to those defaults; protect replaces the whole rule.
  • Redirects. A protected request without the cookie gets a 307 to loginPath with ?next= set to the path and query it asked for.
  • Path forwarding. With next, every request that continues carries x-better-iam-pathname. requireSession, page, and requireTenantSession use it as the default ?next=, so a deep link survives sign-in without passing returnTo by hand. iamNext.currentPath() reads it (when present and safe).

Signed-in redirects and stale cookies

signedInRedirect sends visitors who carry a cookie away from a bare login page, for example to the dashboard. A login URL with ?next= always renders.

That rule is what keeps a stale cookie from looping. When a server guard rejects a request that still carries a session cookie (revoked, expired, or from a reset database), it always attaches ?next= (at least /). The middleware sees ?next=, lets the login page render, and the visitor signs in again instead of bouncing between the guard and the middleware.

Safe redirects

safeRedirectPath(value, fallback = '/') returns value only when it is a same-origin path. It rejects absolute URLs, protocol-relative //host, backslashes, control characters, values over 2048 characters, and dot segments that normalize into //host. Run every next parameter through it before redirecting:

import { safeRedirectPath } from '@better-iam/next/edge';

redirect(safeRedirectPath(searchParams.get('next'), '/dashboard'));

The auth forms and guards already do this. The edge entry also exports matchPath(pattern, pathname), the glob matcher behind publicPaths, and sessionCookieName(secure), which returns the cookie name the middleware looks for.

Next steps

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page