Log out, paste a dashboard URL into the address bar, and watch the status code. If the server returns 200 and lets the client discover that the session is missing, the route was never protected at the server boundary.
That happened to six top-level route groups in one live build. They sat outside the configured Next.js matcher and used client-side layouts, so logged-out visitors received empty application shells. The underlying APIs still returned 401, so no private rows were exposed. The page routes were nevertheless public, their structure was visible, and every visit paid to render a screen the visitor could not use.
Why route lists drift
Many applications maintain two versions of “protected routes”:
- a list used by session or role logic;
- the
config.matcherpatterns that decide where Next.js Proxy runs.
Adding a route to only one list creates a half-guarded page. In Next.js 16, the file is called proxy.ts; older versions called the same convention Middleware. The matcher still decides which paths enter that boundary.
Prefer one broad matcher with an explicit public-route decision inside the proxy when that fits the application. If you use a protected-route allowlist, derive every consumer from one route inventory rather than copying paths into two arrays.
const protectedPrefixes = ['/dashboard', '/settings', '/workspace']
export function proxy(request: NextRequest) {
const protectedRoute = protectedPrefixes.some(prefix =>
request.nextUrl.pathname.startsWith(prefix)
)
if (protectedRoute && !readSessionCookie(request)) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
Proxy is the first check, not the last
Next.js recommends using Proxy for optimistic checks and keeping secure authorization close to the data source. Follow that split:
- Proxy rejects obviously unauthenticated page requests before rendering.
- Server components, route handlers, and the data-access layer verify the current session and permission.
- Database policies restrict the rows the caller can reach.
A protected-looking shell is not a security boundary, and a redirect is not a substitute for API authorization.
Turn the sitemap into a test table
For every non-public route, send an unauthenticated request and assert a redirect to login rather than 200. In the original case, the useful measured contrast was six groups returning 200 instead of the expected temporary redirect.
Run the same inventory against API routes and assert 401 or 403 as appropriate. That test catches both failures: pages accidentally omitted from the matcher and data endpoints that trusted the page to keep callers out.
