Skip to main content

clerkMiddleware() | Nuxt

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.

Configure clerkMiddleware()

By default, the Nuxt SDK automatically adds the clerkMiddleware() helper to your Nuxt application.

To manually configure the middleware:

  1. In your nuxt.config.ts file, under the clerk property, set skipServerMiddleware: true.

    nuxt.config.ts
    export default defineNuxtConfig({
      modules: ['@clerk/nuxt'],
      clerk: {
        skipServerMiddleware: true,
      },
    })
  2. In your server/middleware/ directory, create a file named clerk.ts with the following code:

    server/middleware/clerk.ts
    import { clerkMiddleware } from '@clerk/nuxt/server'
    export default clerkMiddleware()

Protect API routes

You can protect API routes using either or both of the following:

You can also protect multiple routesNuxt.js Icon at once.

Warning

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 routesNuxt.js Icon.

Authentication-based protection

To protect routes based on user authentication status, you can check if the user is signed in by checking the isAuthenticated property on the authNuxt.js Icon 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 routesNuxt.js Icon.

server/middleware/clerk.ts
import { clerkMiddleware } from '@clerk/nuxt/server'

export default clerkMiddleware((event) => {
  const { isAuthenticated } = event.context.auth()
  const isAdminRoute = event.path.startsWith('/api/admin')

  if (!isAuthenticated && isAdminRoute) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized: User not signed in',
    })
  }
})

Authorization-based protection

To protect routes based on user authorization status, you can use the has() helper to check if the user has the required privileges, such as a Role, Permission, Feature, or Plan. The has() helper is available on the authNuxt.js Icon object.

Note

This middleware check is an optional early-rejection layer. The authoritative check belongs on the route handler itself, as shown in Protect multiple routesNuxt.js Icon.

Example: Protect routes based on Custom Permissions

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'

export default clerkMiddleware((event) => {
  const { has } = event.context.auth()
  const isInvoicesRoute = event.path.startsWith('/api/invoices')
  const canCreateInvoices = has({
    permission: 'org:invoices:create',
  })

  // Check if the user is accessing a protected route
  if (isInvoicesRoute) {
    // Check if the user has the required Permission
    if (!canCreateInvoices) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Forbidden: Missing Permission to create invoices',
      })
    }
  }
})

Warning

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'

export default clerkMiddleware((event) => {
  const { has } = event.context.auth()
  const isAdminRoute = event.path.startsWith('/api/admin')
  const isAdmin = has({
    role: 'org:admin',
  })

  // Check if the user is accessing a protected route
  if (isAdminRoute) {
    // Check if the user has the required Role
    if (!isAdmin) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Forbidden: Admin access required',
      })
    }
  }
})

Protect multiple routes

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.

For pages, use Nuxt route middleware. See the guide on protecting content.

Note

Clerk's createRouteMatcher() helper was removed in @clerk/nuxt v3. For migration details, see createRouteMatcher() (removed)Nuxt.js Icon.

The following helpers require authentication and optionally enforce a Permission:

server/utils/requireAuth.ts
import type { OrganizationCustomPermissionKey } from '@clerk/nuxt/types'
import type { H3Event } from 'h3'

export function requireAuth(event: H3Event) {
  const { isAuthenticated, userId, has } = event.context.auth()

  if (!isAuthenticated) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized: User not signed in',
    })
  }

  return { userId, has }
}

export function requirePermission(event: H3Event, permission: OrganizationCustomPermissionKey) {
  const { userId, has } = requireAuth(event)

  if (!has({ permission })) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Forbidden: Missing required Permission',
    })
  }

  return { userId }
}

Each route handler calls the helper that matches its policy:

server/api/admin/users.ts
export default defineEventHandler((event) => {
  const { userId } = requireAuth(event)

  return { userId }
})
server/api/invoices/index.ts
export default defineEventHandler((event) => {
  const { userId } = requirePermission(event, 'org:invoices:create')

  return { userId }
})

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.
const publicRoutes = ['/api/webhooks', '/api/health']

export default clerkMiddleware((event) => {
  const { isAuthenticated } = event.context.auth()
  // Match on the pathname; `event.path` includes the query string
  const { pathname } = getRequestURL(event)

  const isApiRoute = pathname === '/api' || pathname.startsWith('/api/')
  const isPublicRoute = publicRoutes.some(
    (route) => pathname === route || pathname.startsWith(`${route}/`),
  )

  if (isApiRoute && !isPublicRoute && !isAuthenticated) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized: User not signed in',
    })
  }
})

Warning

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'

const isProtectedRoute = createRouteMatcher(['/api/invoices(.*)', '/api/admin(.*)'])

export default clerkMiddleware((event) => {
  const { isAuthenticated } = event.context.auth()

  if (!isAuthenticated && isProtectedRoute(event)) {
    throw createError({
      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.

app/pages/dashboard.vue
<script setup lang="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
export default defineEventHandler((event) => {
  const { isAuthenticated, userId } = event.context.auth()

  if (!isAuthenticated) {
    throw createError({
      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 routesNuxt.js Icon. 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.

clerkMiddleware() options

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.

  • Name
    organizationSyncOptions?
    Type
    OrganizationSyncOptionsNuxt.js Icon | undefined
    Description

    Used to activate a specific Organization or based on URL path parameters. If there's a mismatch between the in the session (e.g., as reported by auth()Next.js Icon) 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 for your instance.

  • Name
    secretKey?
    Type
    string
    Description

    The Clerk for your instance. The CLERK_ENCRYPTION_KEY environment variable must be set when providing secretKey as an option, refer to Dynamic keysNuxt.js Icon.

  • Name
    frontendApiProxy?
    Type
    FrontendApiProxyOptionsNuxt.js Icon
    Description

    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.

OrganizationSyncOptions

The organizationSyncOptions property on the clerkMiddleware()Nuxt.js Icon options object has the type OrganizationSyncOptions, which has the following properties:

  • Name
    organizationPatterns
    Type
    PatternNuxt.js Icon[]
    Description

    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 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 />.

    Common examples:

    • ["/orgs/:slug", "/orgs/:slug/(.*)"]
    • ["/orgs/:id", "/orgs/:id/(.*)"]
    • ["/app/:any/orgs/:slug", "/app/:any/orgs/:slug/(.*)"]
  • Name
    personalAccountPatterns
    Type
    PatternNuxt.js Icon[]
    Description

    URL patterns for resources that exist within the context of a user's .

    If the route also matches the organizationPattern prop, the organizationPattern prop takes precedence.

    Common examples:

    • ["/me", "/me/(.*)"]
    • ["/user/:any", "/user/:any/(.*)"]

Pattern

A Pattern is a string that represents the structure of a URL path. In addition to any valid URL, it may include:

  • Named path parameters prefixed with a colon (e.g., :id, :slug, :any).
  • Wildcard token, (.*), which matches the remainder of the path.

Examples

  • /orgs/:slug
URLMatches:slug value
/orgs/acmecorpacmecorp
/orgsn/a
/orgs/acmecorp/settingsn/a
  • /app/:any/orgs/:id
URLMatches:id value
/app/petstore/orgs/org_123org_123
/app/dogstore/v2/orgs/org_123n/a
  • /personal-account/(.*)
URLMatches
/personal-account/settings
/personal-account

Feedback

What did you think of this content?

Last updated on