The clerkMiddleware() helper allows you to protect your Nuxt application on the server-side. It can be used to validate a user's authentication status or authorization status.
Warning
clerkMiddleware() should be used to protect API routes only. It's not recommended to use it to protect pages as it will only work on initial page reload. On subsequent navigations, it won't run, because client-side navigation doesn't reach the server middleware. To protect pages, see the guide on protecting content from unauthenticated users.
Checks in clerkMiddleware() run before routing and rely on path matching, which can be bypassed with URL-encoding tricks. Treat them as an early rejection layer and keep the authoritative check on the route handler itself, as shown in Protect multiple routes.
To protect routes based on user authentication status, you can check if the user is signed in by checking the isAuthenticated property on the auth object.
In the following example, the clerkMiddleware() helper checks if the user is signed in and accessing a protected route. If they aren't signed in, an error is thrown using Nuxt's createError() utility.
Note
This middleware check is an optional early-rejection layer. The authoritative check belongs on the route handler itself, as shown in Protect multiple routes.
server/middleware/clerk.ts
import { clerkMiddleware } from'@clerk/nuxt/server'exportdefaultclerkMiddleware((event) => {const { isAuthenticated } =event.context.auth()constisAdminRoute=event.path.startsWith('/api/admin')if (!isAuthenticated && isAdminRoute) {throwcreateError({ statusCode:401, statusMessage:'Unauthorized: User not signed in', }) }})
This middleware check is an optional early-rejection layer. The authoritative check belongs on the route handler itself, as shown in Protect multiple routes.
In the following example, the clerkMiddleware() helper checks if the user is accessing a protected route. If so, it checks if the user has the required Custom Permission. If they don't, an error is thrown using Nuxt's createError() utility.
server/middleware/clerk.ts
import { clerkMiddleware } from'@clerk/nuxt/server'exportdefaultclerkMiddleware((event) => {const { has } =event.context.auth()constisInvoicesRoute=event.path.startsWith('/api/invoices')constcanCreateInvoices=has({ permission:'org:invoices:create', })// Check if the user is accessing a protected routeif (isInvoicesRoute) {// Check if the user has the required Permissionif (!canCreateInvoices) {throwcreateError({ statusCode:403, statusMessage:'Forbidden: Missing Permission to create invoices', }) } }})
It's best practice to use Permission-based authorization over Role-based authorization, as it reduces complexity and increases security. Usually, complex Role checks can be refactored with a single Permission check.
In the following example, the clerkMiddleware() helper checks if the user is accessing a protected route. If so, it checks if the user has the required admin Role. If they don't, an error is thrown using Nuxt's createError() utility.
server/middleware/clerk.ts
import { clerkMiddleware } from'@clerk/nuxt/server'exportdefaultclerkMiddleware((event) => {const { has } =event.context.auth()constisAdminRoute=event.path.startsWith('/api/admin')constisAdmin=has({ role:'org:admin', })// Check if the user is accessing a protected routeif (isAdminRoute) {// Check if the user has the required Roleif (!isAdmin) {throwcreateError({ statusCode:403, statusMessage:'Forbidden: Admin access required', }) } }})
To reuse an authentication or authorization policy across API routes, define a helper in server/utils/ and call it from each handler. Nitro auto-imports exports from this directory. Because each resolved handler enforces the policy directly, authorization doesn't depend on matching request paths in server middleware.
You can also reject unauthenticated API requests early in clerkMiddleware(), before they reach any handler. List the routes that are public and require authentication for everything else, so new API routes are protected by default. Middleware sees the request path before routing, and path matching there can diverge from where Nitro actually routes the request (/api/%61dmin won't match a check for /api/admin but can still route to it), so treat this as an extra layer on top of per-resource checks, never as the only protection.
server/middleware/clerk.ts
import { clerkMiddleware } from'@clerk/nuxt/server'// Public API routes. Everything else under `/api` requires authentication.constpublicRoutes= ['/api/webhooks','/api/health']exportdefaultclerkMiddleware((event) => {const { isAuthenticated } =event.context.auth()// Match on the pathname; `event.path` includes the query stringconst { pathname } =getRequestURL(event)constisApiRoute= pathname ==='/api'||pathname.startsWith('/api/')constisPublicRoute=publicRoutes.some( (route) => pathname === route ||pathname.startsWith(`${route}/`), )if (isApiRoute &&!isPublicRoute &&!isAuthenticated) {throwcreateError({ statusCode:401, statusMessage:'Unauthorized: User not signed in', }) }})
createRouteMatcher() was removed in @clerk/nuxt v3. Middleware-based auth checks rely on path matching, which can diverge from how Nitro routes requests and leave protected resources reachable. Move auth checks onto the resources themselves, as shown below.
createRouteMatcher() was a Clerk helper function that accepted an array of routes and checked if the route the user was trying to visit matched one of them, so that auth checks could run in the middleware for matching routes:
server/middleware/clerk.ts
import { clerkMiddleware, createRouteMatcher } from'@clerk/nuxt/server'constisProtectedRoute=createRouteMatcher(['/api/invoices(.*)','/api/admin(.*)'])exportdefaultclerkMiddleware((event) => {const { isAuthenticated } =event.context.auth()if (!isAuthenticated &&isProtectedRoute(event)) {throwcreateError({ statusCode:401, statusMessage:'Unauthorized: User not signed in', }) }})
It was also auto-imported on the client for use inside Nuxt route middleware to match pages.
To migrate, move the auth check into each resource the matcher protected. clerkMiddleware() itself is still required for Clerk to work; it's added automatically unless you set skipServerMiddleware. Role and Permission checks with has() also move onto the resource. Middleware logic unrelated to auth protection, such as locale redirects or headers, can stay, using plain path checks with getRequestURL(event).pathname. Plain path checks don't normalize percent-encoding (/api/%61dmin won't match a check for /api/admin), so never rely on them as a resource's only protection.
Nuxt page
API route
app/pages/dashboard.vue
<scriptsetuplang="ts">// `auth` is a named route middleware in `app/middleware/auth.ts` that// redirects signed-out users.definePageMeta({ middleware:'auth' })</script><template> <h1>Dashboard</h1><!-- Renders child routes, which inherit the `auth` middleware --> <NuxtPage /></template>
server/api/invoices/index.ts
exportdefaultdefineEventHandler((event) => {const { isAuthenticated,userId } =event.context.auth()if (!isAuthenticated) {throwcreateError({ statusCode:401, statusMessage:'Unauthorized: User not signed in', }) }return { userId }})
If several API routes share the same policy, define the check once in a shared helper, as shown in Protect multiple routes. For page-protection patterns, including the named auth route middleware and route groups, see the guide on protecting content.
If you want to hand this migration to a coding agent, use the following prompt:
Migrate my Nuxt project away from Clerk's removed `createRouteMatcher` API.1. Find every matcher created with `createRouteMatcher`, along with the logic that uses it (throwing 401 errors, calling `navigateTo('/sign-in')`, etc.). Matchers can appear in Nitro server middleware (imported from`@clerk/nuxt/server`) or in Nuxt route middleware (auto-imported).2. For every resource those matchers protected, move the auth check onto the resource itself. If a matcher was used inverted (e.g. `if (!isPublicPage(to))`), the protected set is every route it does not match, so every non-public resource needs a check: - In API routes and server handlers, add this at the top of the handler: const { isAuthenticated } = event.context.auth(); if (!isAuthenticated) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); - For pages, create a named route middleware in `app/middleware/` that checks`useAuth()` and redirects with `navigateTo()`, then opt pages into it with`definePageMeta({ middleware: 'auth' })`. Child routes inherit the middleware applied to their parent. - Keep any role or permission checks (`event.context.auth().has(...)`) with the resource as well.3. Remove the `createRouteMatcher` imports and calls. Keep `clerkMiddleware()` itself (it's added automatically unless `skipServerMiddleware` is set). Middleware logic unrelated to auth protection (locale redirects, headers, etc.) may stay, using plain `getRequestURL(event).pathname` checks. Plain pathname checks do not normalize percent-encoding (`/api/%61dmin` will not match a check for `/api/admin`), so never use them for auth or security decisions. Those belong on the resource itself, as in step 2.4. Ensure every page and endpoint previously covered by a matcher pattern (including glob patterns like `/dashboard(.*)`) now has its own check, then verify the project builds.
The clerkMiddleware() function accepts an optional object. The following options are available:
Name
audience?
Type
string | string[]
Description
A string or list of audiences. If passed, it is checked against the aud claim in the token.
Name
authorizedParties?
Type
string[]
Description
An allowlist of origins to verify against, to protect your application from the subdomain cookie leaking attack. For example: ['http://localhost:3000', 'https://example.com']
Name
clockSkewInMs?
Type
number
Description
Specifies the allowed time difference (in milliseconds) between the Clerk server (which generates the token) and the clock of the user's application server when validating a token. Defaults to 5000 ms (5 seconds).
Name
domain?
Type
string
Description
The domain used for satellites to inform Clerk where this application is deployed.
Name
isSatellite?
Type
boolean
Description
When using Clerk's satellite feature, this should be set to true for secondary domains.
Name
satelliteAutoSync?
Type
boolean
Description
Controls whether a satellite app automatically syncs authentication state with the primary domain on first page load. When false (default), the satellite app skips the automatic redirect if no session cookies exist, and only triggers the handshake after the user initiates a sign-in or sign-up action. When true, the satellite app redirects to the primary domain on every first visit to sync state. Defaults to false. See satellite domains for more details.
Name
jwtKey
Type
string
Description
Used to verify the session token in a networkless manner. Supply the JWKS Public Key from the API keys page in the Clerk Dashboard. It's recommended to use the environment variable instead. For more information, refer to Manual JWT verification.
Used to activate a specific Organization or Personal Account based on URL path parameters. If there's a mismatch between the Active Organization in the session (e.g., as reported by auth()) and the Organization indicated by the URL, the middleware will attempt to activate the Organization specified in the URL.
Name
proxyUrl?
Type
string
Description
Specify the URL of the proxy, if using a proxy.
Name
signInUrl
Type
string
Description
The full URL or path to your sign-in page. Needs to point to your primary application on the client-side. Required for a satellite application in a development instance. It's recommended to use the environment variable instead.
Name
signUpUrl
Type
string
Description
The full URL or path to your sign-up page. Needs to point to your primary application on the client-side. Required for a satellite application in a development instance. It's recommended to use the environment variable instead.
Name
publishableKey
Type
string
Description
The Clerk Publishable Key for your instance.
Name
secretKey?
Type
string
Description
The Clerk Secret Key for your instance. The CLERK_ENCRYPTION_KEY environment variable must be set when providing secretKey as an option, refer to Dynamic keysNuxt.js Icon.
Configure Frontend API proxy handling. When enabled, requests to the proxy path are forwarded to Clerk's Frontend API, and the proxyUrl is automatically derived for authentication handshake.
The organizationSyncOptions property on the clerkMiddleware()Nuxt.js Icon options
object has the type OrganizationSyncOptions, which has the following properties:
Specifies URL patterns that are Organization-specific, containing an Organization ID or slug as a path parameter. If a request matches this path, the Organization identifier will be used to set that Organization as active.
If the route also matches the personalAccountPatterns prop, this prop takes precedence.
Patterns must have a path parameter named either :id (to match a Clerk Organization ID) or :slug (to match a Clerk Organization slug).
Warning
If the Organization can't be activated—either because it doesn't exist or the user lacks access—the previously Active Organization will remain unchanged. Components must detect this case and provide an appropriate error and/or resolution pathway, such as calling notFound() or displaying an <OrganizationSwitcher />.