Migrate away from Middleware-based auth checks
Previously, authentication checks were often performed in clerkMiddleware() using createRouteMatcher(). Clerk is deprecating this approach and now recommends protecting each server-side resource (pages, Route Handlers, and Server Functions, including Server Actions) individually, rather than relying on Middleware-based auth checks. This guide explains how to migrate to the new resource-based protection model. To learn more about why createRouteMatcher() is being deprecated, see Motivations for deprecating createRouteMatcher().
createRouteMatcher() continues to work and its behavior hasn't changed. Calling it logs a one-time deprecation warning to the console in development, and the function will be removed in the next major version of @clerk/nextjs. You can plan the migration on your own schedule, but complete it before upgrading to the next major version.
Before you start
Before you migrate, it's important to understand how resource protection works and how removing Middleware-based authentication checks can affect your signed-out user experience.
This migration is about both protecting server-side resources and preserving the signed-out user experience. Server-rendered pages, Route Handlers, Server Functions, external API endpoints, and any other code that can access privileged data must include their own authentication checks.
Client Components can't reach privileged data directly, but this migration still applies to routes that render Client Components. Client-only routes might need a signed-out state even if there is no server-side resource to protect.
Use the following guides to learn how to protect different types of resources:
- To require a signed-in user, see the guide on protecting content from unauthenticated users.
- To require a specific Role, Permission, Feature, or Plan, see the guide on authorization checks.
- For machine requests (API keys, OAuth tokens, machine tokens), check the token type in the Route Handler with auth({ acceptsToken }).
- To understand how to protect external API endpoints, see the guide on making authenticated requests as well as the SDK reference for the programming language or framework your backend API uses.
Migration guide
Don't remove clerkMiddleware() entirely during this migration. It's still required for Clerk to work correctly and can still be used for any non-auth logic, like redirects, headers, and other request handling. If you use createRouteMatcher() for non-auth path logic, replace it with the framework's native matching (config.matcher and req.nextUrl.pathname). For an example, see Geo blocking.
The line to hold: if deleting a piece of clerkMiddleware() logic would leave a resource unprotected, it was a gate and belongs at the resource. A cross-cutting redirect for incomplete session tasks or onboarding can stay in clerkMiddleware() on that basis: the redirect only sends the user somewhere useful first, while the resource still runs the auth check.
To help with the migration, Clerk has launched @clerk/eslint-plugin@clerk/next/require-auth-protection, a lint rule that warns you when a resource is missing required protection. The package also includes the Bulk Fixer CLI, which can automatically add protection to your resources. You can also migrate manually if you prefer. Use the following tabs to choose your migration method.
Install and configure the lint rule
Install @clerk/eslint-plugin. The plugin requires ESLint >=9 with the flat config format (Oxlint is also supportedrequire-auth-protection rule is experimental, so pin the package version. See the @clerk/eslint-plugin reference
npm install --save-dev @clerk/eslint-pluginpnpm add --save-dev @clerk/eslint-pluginyarn add --dev @clerk/eslint-pluginbun add --dev @clerk/eslint-pluginIn your eslint.config.mjs file, register the Next.js plugin and the @clerk/next/require-auth-protection rule. The rule accepts a protected option which is an array of folder globs whose resources must perform an authentication check. You can base the patterns on your current createRouteMatcher() patterns, but you need to convert them from URL-based patterns to folder-based patterns.
Derive the folder globs from your actual directory tree, not from the URL patterns, because folder paths and URLs don't always match:
Route groups and dynamic segments like [locale] appear in folder globs but not in URLs. A glob that matches no folders reports no lint errors, which can look like a fully protected slice, so double-check each glob against your directory structure. These examples assume a src directory; see Path matching
It's recommended to incrementally migrate your application, starting with a small slice of your application and gradually adding more resources to the protected set. You can also temporarily disable resource groups that you aren't ready to migrate yet.
Scope protected to the slice you're migrating. The following example scopes the rule to a /dashboard folder and temporarily skips Server Component entrypoints so you can focus on Route Handlers and Server Functions first:
import clerkNext from '@clerk/eslint-plugin/next'
export default [
{
plugins: { '@clerk/next': clerkNext },
rules: {
'@clerk/next/require-auth-protection': [
'error',
{
protected: ['src/app/dashboard/**', 'src/actions/dashboard/**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
resources: {
routeHandlers: true,
serverFunctions: true,
serverComponentEntrypoints: false, // Skip for now.
},
},
],
},
},
]When you're ready to include pages, layouts, templates, and default files in the migration, set serverComponentEntrypoints to true or remove the resources option altogether.
If you're migrating the whole application at once, you can set protected to include all route groups:
import clerkNext from '@clerk/eslint-plugin/next'
export default [
{
plugins: { '@clerk/next': clerkNext },
rules: {
'@clerk/next/require-auth-protection': [
'error',
{
protected: ['**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
},
],
},
},
]Run the Bulk Fixer CLI
The lint rule doesn't use ESLint autofixes, because adding protection changes runtime behavior. Instead, you can run the Bulk Fixer CLI
The Bulk Fixer CLI runs ESLint with your existing config, respects your protected, public, and resources options, and applies the rule's await auth.protect() suggestion anywhere it can do so safely.
Run the fixer with npx:
# Fix everything under the current directory.
npx clerk-next-fix-auth-protection
# Preview without writing files.
npx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
npx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
pnpm dlx clerk-next-fix-auth-protection
# Preview without writing files.
pnpm dlx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
pnpm dlx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
yarn dlx clerk-next-fix-auth-protection
# Preview without writing files.
yarn dlx clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
yarn dlx clerk-next-fix-auth-protection "src/**"# Fix everything under the current directory.
bun x clerk-next-fix-auth-protection
# Preview without writing files.
bun x clerk-next-fix-auth-protection --dry-run
# Scope fixes to a specific path or glob.
bun x clerk-next-fix-auth-protection "src/**"The command exits with a non-zero status when:
--dry-runreports changes that would be made.- Some resources need manual attention.
The following examples demonstrate the resource-based checks that the Bulk Fixer CLI inserts.
When a user isn't authenticated, auth.protect()401 for Server Actions, and returns a 404 for other non-document requests (such as Route Handlers).
If you want more control over the response, or if you want to perform instead, adjust resources manually.
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
await auth.protect()
return <h1>Dashboard</h1>
}import { auth } from '@clerk/nextjs/server'
export async function GET() {
await auth.protect()
return Response.json({ ok: true })
}'use server'
import { auth } from '@clerk/nextjs/server'
export async function updateDashboard() {
await auth.protect()
// Add your Server Function logic
}The Bulk Fixer CLI will warn about resources that require manual attention, such as imported or wrapped exports, or mixed-scope layouts that require explicit acknowledgement. Add protection manually to those resources when they still need it.
If the resource is already protected by a wrapper or by code in another file, the rule can't currently verify that protection. In that case, add an ESLint disable comment with a reason:
// eslint-disable-next-line @clerk/next/require-auth-protection -- Protected by withAuth().
export const POST = withAuth(handler)If a route only uses Client Components, the protected resource is often an API endpoint that the client calls. If the Middleware-based authentication check previously redirected signed-out users before that call happened, removing it without adding a signed-out state can make the UX worse. For example, a page that previously redirected might now show a generic error page instead.
Use client-side controls such as <Show when="signed-out"> to handle a signed-out user's experience.
One way to preserve the previous UX is to render signed-out handling from the shared layout for the affected routes. Keep this logic in a Client Component so it can respond to client-side auth state changes.
'use client'
import { RedirectToSignIn, Show } from '@clerk/nextjs'
export default function SignedOutRedirect({ children }: { children: React.ReactNode }) {
return (
<>
<Show when="signed-in">{children}</Show>
<Show when="signed-out">
<RedirectToSignIn />
</Show>
</>
)
}Use this pattern in the layout for the affected route or route group, rather than in a root layout that also renders public pages. This preserves the signed-out experience, but it doesn't replace auth checks in the API endpoints the client calls.
import SignedOutRedirect from './SignedOutRedirect'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return <SignedOutRedirect>{children}</SignedOutRedirect>
}Remove the Middleware authentication checks
After the server resources and signed-out states are handled, remove the Middleware authentication checks for the routes you've migrated. If you're migrating incrementally, keep the checks for the routes you haven't migrated yet. It's safe for a route to be protected by both the Middleware check and the resource check while the migration is in progress.
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
// The `/dashboard` patterns were removed after that slice was migrated.
// Keep the remaining patterns until their routes are migrated.
const isProtected = createRouteMatcher(['/settings(.*)', '/admin(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) await auth.protect()
})Once every slice has been migrated, remove the authentication checks entirely:
import { clerkMiddleware } from '@clerk/nextjs/server'
// Keep clerkMiddleware() and your existing `config.matcher` export;
// they're still required. Only remove the authentication checks.
export default clerkMiddleware()Test and repeat
Test the migrated slice while signed out. Verify that protected resources reject unauthenticated requests and that pages still show the expected signed-out experience.
If you have more slices to migrate, start from the top of the guide and add more patterns to the lint rule. Repeat until you are no longer using createRouteMatcher().
Keep the lint rule enabled for ongoing protection
Once the migration is complete, broaden protected to cover your whole application, opting out only the public routes.
import clerkNext from '@clerk/eslint-plugin/next'
export default [
{
plugins: { '@clerk/next': clerkNext },
rules: {
'@clerk/next/require-auth-protection': [
'error',
{
protected: ['**'],
public: ['src/app/sign-in/**', 'src/app/sign-up/**'],
},
],
},
},
]If you prefer to migrate manually, it's still recommended to install the lint rule to help ensure your resources stay protected over time.
Identify a slice of your application to migrate
Choose a route, route group, or feature area to migrate first. Within that slice, identify every resource that needs protection. You can use your current createRouteMatcher() patterns as a starting point.
For routes that require signed-in users, check the following files:
- Protect all
layout,template,default, andpagefiles.- Protecting only layout files isn't enough, because they don't always re-render when pages do.
- Protect
loadinganderrorfiles only if they access sensitive resources.- These files are usually mostly static and often don't need protection, but there are exceptions.
- Protect exports from
routefiles.- All functions named as HTTP verbs turn into Route Handlers, so protect them.
Server Functions need protection regardless of where they are located, so audit them separately.
Add auth checks to server resources
The following example shows how to migrate a Middleware-based auth check to a resource-based auth check. This example performs an authentication check on the resource, but the same principle applies to any authentication or that you want to move to the resource.
If clerkMiddleware() protects a route:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtected = createRouteMatcher(['/dashboard/:path*'])
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) await auth.protect()
})Move the auth check into each protected resource. The following examples use auth.protect().
When a user isn't authenticated, auth.protect()401 for Server Actions, and returns a 404 for other non-document requests (such as Route Handlers).
If you want more control over the response, or if you want to perform instead, see the relevant guides in the Before you start section.
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
await auth.protect()
return <h1>Dashboard</h1>
}import { auth } from '@clerk/nextjs/server'
export async function GET() {
const { userId } = await auth.protect()
return Response.json({ userId })
}'use server'
import { auth } from '@clerk/nextjs/server'
export async function updateDashboard() {
const { userId } = await auth.protect()
// Add your Server Function logic using the `userId`
}If a route only uses Client Components, the protected resource is often an API endpoint that the client calls. If the Middleware-based authentication check previously redirected signed-out users before that call happened, removing it without adding a signed-out state can make the UX worse. For example, a page that previously redirected might now show a generic error page instead.
Use client-side controls such as <Show when="signed-out"> to handle a signed-out user's experience.
One way to preserve the previous UX is to render signed-out handling from the shared layout for the affected routes. Keep this logic in a Client Component so it can respond to client-side auth state changes.
'use client'
import { RedirectToSignIn, Show } from '@clerk/nextjs'
export default function SignedOutRedirect({ children }: { children: React.ReactNode }) {
return (
<>
<Show when="signed-in">{children}</Show>
<Show when="signed-out">
<RedirectToSignIn />
</Show>
</>
)
}Use this pattern in the layout for the affected route or route group, rather than in a root layout that also renders public pages. This preserves the signed-out experience, but it doesn't replace auth checks in the API endpoints the client calls.
import SignedOutRedirect from './SignedOutRedirect'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return <SignedOutRedirect>{children}</SignedOutRedirect>
}Remove the Middleware authentication checks
After the server resources and signed-out states are handled, remove the Middleware authentication checks for the routes you've migrated. If you're migrating incrementally, keep the checks for the routes you haven't migrated yet. It's safe for a route to be protected by both the Middleware check and the resource check while the migration is in progress.
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
// The `/dashboard` patterns were removed after that slice was migrated.
// Keep the remaining patterns until their routes are migrated.
const isProtected = createRouteMatcher(['/settings(.*)', '/admin(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) await auth.protect()
})Once every slice has been migrated, remove the authentication checks entirely:
import { clerkMiddleware } from '@clerk/nextjs/server'
// Keep clerkMiddleware() and your existing `config.matcher` export;
// they're still required. Only remove the authentication checks.
export default clerkMiddleware()Test and repeat
Test the migrated slice while signed out. Verify that protected resources reject unauthenticated requests and that pages still show the expected signed-out experience.
If you have more slices to migrate, start from the top of the guide and repeat the process until you are no longer using createRouteMatcher().
What about early redirects for signed-out users?
Moving signed-out UX handling out of Middleware can raise a natural performance question: "Will signed-out users now have to wait longer for the redirect to happen, especially if it happens in a Client Component?"
This is a fair concern. If this is something you want to optimize for, first follow this migration guide to ensure your application behaves correctly, even without the Middleware gate. After that, you can add back an early redirect to the Middleware gate as a pure performance optimization.
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware(async (auth, req) => {
// Important: This is not an auth guarantee, only
// a performance optimization for signed-out users.
const pathname = req.nextUrl.pathname
if (pathname === '/dashboard' || pathname.startsWith('/dashboard/')) {
const { isAuthenticated, redirectToSignIn } = await auth()
if (!isAuthenticated) return redirectToSignIn()
}
})Keep the resource-level auth checks in place. Because the early redirect is a pure performance optimization, you can remove it at any time without exposing protected data or changing access control. The only impact of removing it is on signed-out users' perceived load time.
The reasons for deprecating createRouteMatcher() are a mix of existing concerns and new ones.
One main concern has always been that Middleware-level auth gating can lead to a false sense of security. It can be tempting to think, "I'm already guarding my entire application in Middleware, so I'm safe by default." In reality, Middleware checks can be bypassed in several ways, leaving resources unprotected.
Server Functions are one example. It can be tempting to think these are protected by Middleware checks, but they're called by ID, not by path. If a Server Function that lives in /protected/server-functions.ts is called while visiting /public/, Middleware sees /public/ and lets the request through, even if /protected/:path* is protected.
Another example is the mapping of regular expressions first to URLs and then to folders. Regular expressions have subtle caveats, and a mistake can leave a resource unprotected.
Differences between how createRouteMatcher() parses a path and how the framework resolves it can also lead to dangerous gaps. If Clerk normalizes a path one way and treats it as public, but the framework normalizes it differently and renders a protected path, that creates a bypass. This is what happened in the GHSA-vqx2-fgx2-5wq9 vulnerability. Future framework changes to path interpretation can introduce new vulnerabilities, and patching them inside createRouteMatcher() isn't sustainable without first-class framework support.
On top of these concerns, in 2025, several framework-level Middleware bypasses were publicly disclosed. In these cases, the request never goes through createRouteMatcher() at all, and all authentication checks at the Middleware level are bypassed.
Together, these concerns led Clerk to deprecate createRouteMatcher() and recommend moving away from Middleware-level auth gating.
Clerk strongly believes in being secure by default, and this change helps provide those guarantees without creating a false sense of security.
Feedback
Last updated on