There are five places a check can go, and they are not interchangeable.
| Place | Sees | Good for | Cannot be trusted to |
|---|---|---|---|
| proxy.ts | the cookie | redirecting anonymous visitors | authorize anything |
| layout.js | request APIs | shell UI, navigation | guard nested segments |
| page.js | request APIs | choosing what to render | guard actions on that page |
| Server Action | request APIs | mutations | be skipped, ever |
| Route handler | request APIs | API responses | inherit a page's check |
The proxy is first and cheapest, so a redirect belongs there. It runs on every matched request, prefetches included, so it must read the cookie and nothing else — a database round trip there is paid on every hover. That is an optimistic check: a signed cookie decides where to send a browser, not what data to hand out.
export default async function proxy(request: NextRequest) {
const session = await getIronSession<SessionData>(await cookies(), sessionOptions);
if (request.nextUrl.pathname.startsWith("/dashboard") && !session.userId) {
return NextResponse.redirect(new URL("/login", request.nextUrl));
}
return NextResponse.next();
}
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };$ curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" localhost:3319/dashboard
307 -> http://localhost:3319/loginNow the limits. A matcher is a path filter, and Server Functions are not paths: excluding /admin from the matcher also removes proxy coverage from every action defined there.
Layouts are the second trap. A layout runs when a user first lands inside its segment and then does not re-run on client-side navigations within it, because the router re-renders only the segments that changed: a session that expires mid-visit is never re-checked. Worse, the router renders the page and any parallel slots itself, so a layout that returns null for an unauthorized user stops nothing — the page ran anyway, and its output is in the RSC payload.
That leaves pages, Server Actions and route handlers. Put the check in all three, behind one function.