# 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()`](#motivations-for-deprecating-create-route-matcher).

`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](https://clerk.com/docs/guides/secure/protect-content.md).
- To require a specific Role, Permission, Feature, or Plan, see the [guide on authorization checks](https://clerk.com/docs/guides/secure/authorization-checks.md).
- For machine requests (API keys, OAuth tokens, machine tokens), check the token type in the Route Handler with [auth({ acceptsToken })](https://clerk.com/docs/reference/nextjs/app-router/auth.md#verify-machine-requests).
- To understand how to protect external API endpoints, see the [guide on making authenticated requests](https://clerk.com/docs/guides/development/making-requests.md) as well as the SDK reference for the programming language or framework your backend API uses.

> Before you start the migration, verify that all relevant Server Functions are protected. Server Functions can't be reliably protected by Middleware patterns — they're called by ID, not by path (see [Motivations for deprecating `createRouteMatcher()`](#motivations-for-deprecating-create-route-matcher)). Verifying their protection first gives you a safer baseline before removing Middleware checks elsewhere.
>
> The lint rule can help with this step, or you can search your codebase for `'use server'` to find your Server Functions.

## 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](https://clerk.com/docs/guides/development/geo-blocking.md).

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](https://clerk.com/docs/guides/configure/session-tasks.md) or [onboarding](https://clerk.com/docs/guides/development/add-onboarding-flow.md) 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](https://clerk.com/docs/reference/nextjs/eslint-plugin.md), which includes `@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.

> The lint rule and the Bulk Fixer CLI only work with App Router resources, so Pages Router routes must be audited manually. Protect pages with [getAuth()](https://clerk.com/docs/reference/nextjs/pages-router/get-auth.md) in `getServerSideProps`, and protect `pages/api` routes by calling `getAuth(req)` in each handler and returning a `401` for signed-out requests. See the [Pages Router examples](https://clerk.com/docs/guides/users/reading.md#pages-router) for both patterns.

**Use the lint rule**

1. ## Install and configure the lint rule

   Install `@clerk/eslint-plugin`. The plugin requires ESLint `>=9` with the flat config format ([Oxlint is also supported](https://clerk.com/docs/reference/nextjs/eslint-plugin.md#setup)); if you can't use flat config, follow the **Migrate manually** tab instead. The `require-auth-protection` rule is experimental, so [pin](https://clerk.com/docs/pinning.md) the package version. See the [@clerk/eslint-plugin reference](https://clerk.com/docs/reference/nextjs/eslint-plugin.md) for all options and advanced configuration, including how to set it up for monorepos. The reference also describes the exact patterns the lint rule recognizes, like how protection must always be at the top of the function.

   ```npm
   npm install --save-dev @clerk/eslint-plugin
   ```

   In 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:

   | Your route                               | URL pattern              | Folder glob                     |
   | ---------------------------------------- | ------------------------ | ------------------------------- |
   | `src/app/dashboard/`                     | `/dashboard(.*)`         | `src/app/dashboard/**`          |
   | `src/app/(app)/dashboard/` (route group) | `/dashboard(.*)`         | `src/app/(app)/dashboard/**`    |
   | `src/app/[locale]/dashboard/` (i18n)     | `/:locale/dashboard(.*)` | `src/app/[locale]/dashboard/**` |

   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](https://clerk.com/docs/reference/nextjs/eslint-plugin.md#path-matching) for glob semantics and other project layouts.

   **Incremental migration**

   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:

   filename: eslint.config.mjs

   ```js
   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.

   **Migrate the whole application at once**

   If you're migrating the whole application at once, you can set `protected` to include all route groups:

   filename: eslint.config.mjs

   ```js
   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/**'],
           },
         ],
       },
     },
   ]
   ```
2. ## 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](https://clerk.com/docs/reference/nextjs/eslint-plugin.md#bulk-fixer-cli) to automatically add protection to your resources.

   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.

   > Applying these fixes changes your application's runtime behavior. It enforces authentication where there potentially was none, and it might override custom authentication checks that were already in place. Review the changes and test your application after running the Bulk Fixer CLI.

   Run the fixer with `npx`:

   ```npm
   # 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/**"
   ```

   The command exits with a non-zero status when:

   - `--dry-run` reports 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()](https://clerk.com/docs/reference/nextjs/app-router/auth.md#auth-protect) redirects them to the sign-in page for document requests (such as pages), returns a `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](https://clerk.com/docs/guides/secure/protect-content.md#server-side), or if you want to perform authorization checks instead, adjust resources manually.

   **Server Component**

   filename: app/dashboard/page.tsx

   ```tsx
   + import { auth } from '@clerk/nextjs/server'

     export default async function Page() {
   +   await auth.protect()

       return <h1>Dashboard</h1>
     }
   ```

   **Route Handler**

   filename: app/api/dashboard/route.ts

   ```tsx
   + import { auth } from '@clerk/nextjs/server'

     export async function GET() {
   +   await auth.protect()

       return Response.json({ ok: true })
     }
   ```

   **Server Function**

   filename: app/dashboard/actions.ts

   ```tsx
     '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:

   ```ts
   // eslint-disable-next-line @clerk/next/require-auth-protection -- Protected by withAuth().
   export const POST = withAuth(handler)
   ```
3. ## Preserve signed-out UX for client-only routes

   > The lint rule can't help you with this step. You need to verify this manually.

   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">](https://clerk.com/docs/reference/components/control/show.md) 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.

   **Client Component**

   filename: app/dashboard/SignedOutRedirect.tsx

   ```tsx
   '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>
       </>
     )
   }
   ```

   **Layout**

   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.

   filename: app/dashboard/layout.tsx

   ```tsx
   import SignedOutRedirect from './SignedOutRedirect'

   export default function DashboardLayout({ children }: { children: React.ReactNode }) {
     return <SignedOutRedirect>{children}</SignedOutRedirect>
   }
   ```
4. ## 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.

   > If you're using Next.js ≤15, name your file `middleware.ts` instead of `proxy.ts`. The code itself remains the same; only the filename changes.

   filename: proxy.ts

   ```tsx
   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:

   filename: proxy.ts

   ```tsx
   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()
   ```
5. ## 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()`.
6. ## 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.

   filename: eslint.config.mjs

   ```js
   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/**'],
           },
         ],
       },
     },
   ]
   ```

**Migrate manually**

If you prefer to migrate manually, it's still recommended to install the lint rule to help ensure your resources stay protected over time.

1. ## 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.

   > If your Middleware uses the inverted pattern (an `isPublicRoute` matcher that protects everything _except_ the listed routes), don't derive the protected set from the matcher array. Every route that's **not** in the public list is currently protected, so each one needs a resource-level check before you remove the Middleware check. Forgetting one silently makes it public. For this pattern, consider using the lint rule instead: setting `protected: ['**']` and listing your public routes in `public` mirrors your existing model.

   For routes that require signed-in users, check the following files:

   - Protect all `layout`, `template`, `default`, and `page` files.
     - Protecting only layout files isn't enough, because they don't always re-render when pages do.
   - Protect `loading` and `error` files 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 `route` files.
     - 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.
2. ## 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](https://clerk.com/docs/guides/secure/protect-content.md#protect-a-page), but the same principle applies to any authentication or authorization check that you want to move to the resource.

   > If you're using Next.js ≤15, name your file `middleware.ts` instead of `proxy.ts`. The code itself remains the same; only the filename changes.

   If `clerkMiddleware()` protects a route:

   filename: proxy.ts

   ```tsx
   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()](https://clerk.com/docs/reference/nextjs/app-router/auth.md#auth-protect) redirects them to the sign-in page for document requests (such as pages), returns a `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 authorization checks instead, see the relevant guides in the [Before you start](#before-you-start) section.

   **Server Component**

   filename: app/dashboard/page.tsx

   ```tsx
   + import { auth } from '@clerk/nextjs/server'

     export default async function Page() {
   +   await auth.protect()

       return <h1>Dashboard</h1>
     }
   ```

   **Route Handler**

   filename: app/api/dashboard/route.ts

   ```tsx
   + import { auth } from '@clerk/nextjs/server'

     export async function GET() {
   +   const { userId } = await auth.protect()

       return Response.json({ userId })
     }
   ```

   **Server Function**

   filename: app/dashboard/actions.ts

   ```tsx
     '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`
     }
   ```
3. ## Preserve signed-out UX for client-only routes

   > The lint rule can't help you with this step. You need to verify this manually.

   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">](https://clerk.com/docs/reference/components/control/show.md) 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.

   **Client Component**

   filename: app/dashboard/SignedOutRedirect.tsx

   ```tsx
   '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>
       </>
     )
   }
   ```

   **Layout**

   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.

   filename: app/dashboard/layout.tsx

   ```tsx
   import SignedOutRedirect from './SignedOutRedirect'

   export default function DashboardLayout({ children }: { children: React.ReactNode }) {
     return <SignedOutRedirect>{children}</SignedOutRedirect>
   }
   ```
4. ## 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.

   filename: proxy.ts

   ```tsx
   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:

   filename: proxy.ts

   ```tsx
   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()
   ```
5. ## 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.

filename: proxy.ts
```tsx
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.

> Adding the redirect back to the Middleware can make it harder to determine if the base resources are correctly protected.
>
> If you add the redirect behavior back to the Middleware, you should have a strategy for verifying the base resources are also protected. For example, by using the `require-auth-protection` lint rule or by setting up auth tests that skip the Middleware redirect.

## Motivations for deprecating `createRouteMatcher()`

> This section provides the technical rationale for the updated recommendations. Reading this section is optional.

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](https://github.com/clerk/javascript/security/advisories/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.

---

## Sitemap

[Overview of all docs pages](https://clerk.com/docs/llms.txt)
