Auth-Gated proxy.ts in Next.js 16
A proxy.ts that checks the session cookie and redirects unauthenticated requests away from protected routes. Node.js runtime, no Edge.
proxy.ts in Next.js 16 replaces the old middleware.ts and runs on the Node.js runtime instead of the Edge. This snippet shows the canonical auth gate pattern: a fast cookie check that redirects unauthenticated users away from protected routes before any rendering happens.
Tested on Next.js 16.0+, Node 24 LTS.
When to Use This
- Gating an entire
/dashboardor/adminsection behind a session check - Redirecting logged-out users to
/sign-inwith a return-to URL - Forcing a tenant-slug check on multi-tenant routes
- Adding a security header layer in front of authenticated routes
Don't use this when you need fine-grained per-resource authorization (do that in the layout or route handler) or when the protected paths are too few to justify a proxy at all (use auth() directly in the layout).
Code
// proxy.ts (project root, next to app/)
import { NextResponse, type NextRequest } from 'next/server';
const PROTECTED_PREFIXES = ['/dashboard', '/admin', '/account'];
const SESSION_COOKIE = 'session-token';
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const isProtected = PROTECTED_PREFIXES.some((p) =>
pathname === p || pathname.startsWith(`${p}/`)
);
if (!isProtected) {
return NextResponse.next();
}
const session = request.cookies.get(SESSION_COOKIE)?.value;
if (!session) {
const signInUrl = new URL('/sign-in', request.url);
signInUrl.searchParams.set('returnTo', `${pathname}${search}`);
return NextResponse.redirect(signInUrl);
}
return NextResponse.next();
}
export const config = {
matcher: [
// Run on every path except static files and Next internals
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};The matcher regex excludes static assets so the proxy doesn't run on every image request. The returnTo query param survives the redirect so your sign-in page can bounce the user back after login.
Usage
In your /sign-in page, read the returnTo and bounce after successful auth:
// app/sign-in/page.tsx
import { redirect } from 'next/navigation';
import { signInAction } from './actions';
export default async function SignInPage({
searchParams,
}: {
searchParams: Promise<{ returnTo?: string }>;
}) {
const { returnTo } = await searchParams;
async function handleSignIn(formData: FormData) {
'use server';
const ok = await signInAction(formData);
if (ok) redirect(returnTo || '/dashboard');
}
return <form action={handleSignIn}>{/* ... */}</form>;
}Caveats
proxy.tslives at the project root, not insideapp/. Same level asnext.config.ts. If you usesrc/, place it atsrc/proxy.ts.- The cookie check is coarse on purpose. It only checks existence, not validity. Validate the token in your layout or server action before trusting it.
- Don't hit the database in proxy.ts. Even on Node.js runtime, this code runs on every request — keep it O(1). Save DB calls for layouts.
- The
matcherexcludes patterns are critical. Without them, the proxy runs on every static asset, which adds latency to images and CSS. - Set the cookie as
httpOnlyandSecureeverywhere. A cookie readable by JS defeats the purpose of this entire pattern.
Related Snippets & Reading
- Geo-Based Redirects in proxy.ts(coming soon) — another proxy.ts pattern, different use case
- Server Action with Zod Validation(coming soon) — for the sign-in form itself
- Goodbye middleware.ts, Hello proxy.ts — the deep dive on why Next.js made this change
Frequently Asked Questions
Why is auth in proxy.ts considered safer than middleware.ts?
proxy.ts runs on the Node.js runtime, not the Edge Runtime. Edge middleware was bypassable under load (CVE-2025-29927) because it could be skipped by certain request shapes. proxy.ts doesn't have that bypass surface, and it can use Node-only libraries like database drivers and JWT verification.
Should you do full auth in proxy.ts or just a coarse check?
Do a coarse check in proxy.ts and a fine-grained check in your route handlers or layouts. proxy.ts should answer 'is there a session at all' in O(1). Layouts should answer 'does this user have permission for this resource' against the database.