or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

backend-api-client.mdclient-components.mdclient-hooks.mderror-handling.mdindex.mdmiddleware-and-route-protection.mdserver-auth-app-router.mdserver-auth-pages-router.mdsetup-and-provider.mdwebhooks.md
tile.json

middleware-and-route-protection.mddocs/

Middleware and Route Protection

Next.js middleware for authentication and authorization with Clerk. The clerkMiddleware() function processes authentication on every request and enables server-side auth checks throughout your application.

Key Information for Agents

Required Setup:

  • middleware.ts file in project root (or src/ directory)
  • CLERK_SECRET_KEY and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY environment variables
  • Matcher configuration required to specify which routes middleware applies to
  • ClerkProvider must wrap application (for client-side integration)

Default Behaviors:

  • Middleware executes on every request matching the matcher pattern
  • auth() function in middleware handler is async and must be awaited
  • auth.protect() throws error that middleware catches (triggers redirect or 404)
  • debug: false by default (set to true for additional logging)
  • Content Security Policy headers injected automatically if contentSecurityPolicy option provided
  • Default sign-in/sign-up URLs: /sign-in and /sign-up (configurable)
  • Default redirect after auth: / (configurable via afterSignInUrl/afterSignUpUrl)

Threading Model:

  • Middleware executes in Edge Runtime (or Node.js runtime) before route handlers
  • auth() function in middleware is async and returns Promise
  • Middleware handler can be async or sync (return Response or Promise<Response>)
  • Multiple auth() calls in same middleware execution share same session state
  • Route matchers are synchronous functions (execute immediately)

Lifecycle:

  • Middleware executes on every request (before route handlers)
  • Auth state is determined per request (processed once per request)
  • auth.protect() throws error that Next.js middleware catches
  • Redirect responses are returned immediately (request doesn't continue)
  • Custom headers can be added to response before returning

Edge Cases:

  • Matcher pattern must exclude static files and Next.js internals (use provided pattern)
  • auth.protect() in middleware: Throws error that triggers redirect/404
  • auth() called multiple times in middleware: Shares same session state
  • Dynamic options callback: Can return different options per request
  • Satellite domain setup: Requires isSatellite, domain, proxyUrl options
  • Organization sync: Automatically syncs organization from URL parameters if configured
  • Content Security Policy: Only injected if contentSecurityPolicy option provided

Exceptions:

  • Missing environment variables causes middleware initialization to fail
  • Invalid matcher pattern causes middleware to not execute
  • auth.protect() throws error (never returns normally if not authenticated)
  • redirectToSignIn() and redirectToSignUp() return Response (don't throw)
  • Invalid route matcher pattern causes runtime errors

Capabilities

clerkMiddleware() Function

Creates Next.js middleware that handles authentication and authorization for your application.

/**
 * Creates Next.js middleware for authentication and authorization
 * Processes authentication on every request
 * Enables server-side auth checks via auth() and auth.protect()
 * @param handler - Optional handler function for custom middleware logic
 * @param options - Optional configuration object or callback
 * @returns NextMiddleware function
 */
function clerkMiddleware(
  handler?: ClerkMiddlewareHandler,
  options?: ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback
): NextMiddleware;

// Alternative signatures
function clerkMiddleware(
  options?: ClerkMiddlewareOptions
): NextMiddleware;

function clerkMiddleware(
  request: NextRequest,
  event: NextFetchEvent
): Promise<Response>;

type ClerkMiddlewareHandler = (
  auth: ClerkMiddlewareAuth,
  request: NextRequest,
  event: NextFetchEvent
) => NextMiddlewareReturn;

type ClerkMiddlewareAuth = (
  options?: GetAuthOptions
) => Promise<ClerkMiddlewareSessionAuthObject>;

interface ClerkMiddlewareSessionAuthObject {
  userId: string | null;
  sessionId: string | null;
  orgId: string | null;
  orgRole: string | null;
  orgSlug: string | null;
  orgPermissions: string[] | null;
  actor: any | null;
  sessionStatus: string;
  getToken: (options?: GetTokenOptions) => Promise<string | null>;
  has: (params: HasParams) => boolean;
  redirectToSignIn: (options?: RedirectOptions) => Response;
  redirectToSignUp: (options?: RedirectOptions) => Response;
}

interface ClerkMiddlewareOptions {
  /**
   * If true, additional debug information will be logged
   * @default false
   */
  debug?: boolean;

  /**
   * Automatically injects Content-Security-Policy headers compatible with Clerk
   */
  contentSecurityPolicy?: ContentSecurityPolicyOptions;

  /**
   * Override publishable key from environment
   */
  publishableKey?: string;

  /**
   * Domain for multi-domain apps (satellite domain setup)
   */
  domain?: string;

  /**
   * Enable satellite domain mode for multi-domain setup
   * @default false
   */
  isSatellite?: boolean;

  /**
   * Proxy URL for Clerk Frontend API in multi-domain setup
   */
  proxyUrl?: string;

  /**
   * Custom sign-in URL for multi-domain setup
   */
  signInUrl?: string;

  /**
   * Custom sign-up URL for multi-domain setup
   */
  signUpUrl?: string;

  /**
   * URL to redirect after successful sign-in
   * @default '/'
   */
  afterSignInUrl?: string;

  /**
   * URL to redirect after successful sign-up
   * @default '/'
   */
  afterSignUpUrl?: string;

  /**
   * Options for syncing organization based on URL parameters
   */
  organizationSyncOptions?: OrganizationSyncOptions;
}

interface OrganizationSyncOptions {
  /**
   * Organization ID parameter name in URL
   * @default 'organizationId'
   */
  organizationIdParam?: string;

  /**
   * Organization slug parameter name in URL
   * @default 'organizationSlug'
   */
  organizationSlugParam?: string;

  /**
   * Personal account ID parameter name in URL
   * @default 'personalAccountId'
   */
  personalAccountIdParam?: string;
}

type ClerkMiddlewareOptionsCallback = (
  req: NextRequest
) => ClerkMiddlewareOptions | Promise<ClerkMiddlewareOptions>;

interface ContentSecurityPolicyOptions {
  /**
   * If true, uses report-only mode
   * @default false
   */
  reportOnly?: boolean;
}

type NextMiddlewareReturn = Response | NextResponse | Promise<Response | NextResponse> | void;

interface GetAuthOptions {
  treatPendingAsSignedOut?: boolean;
  acceptsToken?: 'session_token' | 'm2m_token' | 'oauth_token' | 'any';
}

interface GetTokenOptions {
  template?: string;
  skipCache?: boolean;
}

interface HasParams {
  permission?: string;
  role?: string;
}

interface RedirectOptions {
  returnBackUrl?: string | URL | null;
}

Usage Example - Basic Setup:

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

export default clerkMiddleware();

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
};

Usage Example - With Handler Function:

// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId } = await auth();

  // Allow public routes
  if (req.nextUrl.pathname.startsWith('/public')) {
    return NextResponse.next();
  }

  // Protect all other routes
  if (!userId) {
    return auth().redirectToSignIn();
  }

  return NextResponse.next();
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - With Options:

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

export default clerkMiddleware({
  debug: true,
  signInUrl: '/sign-in',
  signUpUrl: '/sign-up',
  afterSignInUrl: '/dashboard',
  afterSignUpUrl: '/onboarding',
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Route-Specific Logic:

// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId, orgId } = await auth();
  const path = req.nextUrl.pathname;

  // Public routes - allow everyone
  if (path.startsWith('/public') || path === '/') {
    return NextResponse.next();
  }

  // Auth routes - redirect if already signed in
  if (path.startsWith('/sign-in') || path.startsWith('/sign-up')) {
    if (userId) {
      return NextResponse.redirect(new URL('/dashboard', req.url));
    }
    return NextResponse.next();
  }

  // Protected routes - require authentication
  if (!userId) {
    return auth().redirectToSignIn();
  }

  // Organization routes - require active organization
  if (path.startsWith('/org') && !orgId) {
    return NextResponse.redirect(new URL('/select-org', req.url));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Dynamic Options:

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

export default clerkMiddleware(
  async (auth, req) => {
    // Middleware handler logic
    return;
  },
  async (req) => {
    // Dynamic options based on request
    const isProduction = req.nextUrl.hostname.includes('production.com');

    return {
      debug: !isProduction,
      signInUrl: isProduction ? '/login' : '/sign-in',
      publishableKey: isProduction
        ? process.env.PROD_CLERK_PUBLISHABLE_KEY
        : process.env.DEV_CLERK_PUBLISHABLE_KEY,
    };
  }
);

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Content Security Policy:

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

export default clerkMiddleware({
  contentSecurityPolicy: {
    reportOnly: false,
  },
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

createRouteMatcher() Function

Creates a route matcher function for protecting specific routes based on patterns.

/**
 * Creates route matcher for protecting specific routes
 * Supports glob patterns, regex, and custom functions
 * @param routes - Route patterns to match
 * @returns Function that checks if request matches routes
 */
function createRouteMatcher(
  routes: RouteMatcherParam
): (req: NextRequest) => boolean;

type RouteMatcherParam =
  | Array<RegExp | string>
  | RegExp
  | string
  | ((req: NextRequest) => boolean);

// String patterns support glob syntax:
// - '/admin' - exact match
// - '/admin(.*)' - starts with /admin
// - '/admin/*/settings' - wildcard match
// - '/api/:path*' - Next.js typed route

Usage Example - Basic Pattern:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

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

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Multiple Matchers:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher([
  '/',
  '/about',
  '/contact',
  '/sign-in(.*)',
  '/sign-up(.*)',
]);

const isAdminRoute = createRouteMatcher(['/admin(.*)']);

export default clerkMiddleware(async (auth, req) => {
  const { userId, has } = await auth();

  // Allow public routes
  if (isPublicRoute(req)) {
    return;
  }

  // Require authentication for all other routes
  if (!userId) {
    return auth().redirectToSignIn();
  }

  // Require admin role for admin routes
  if (isAdminRoute(req) && !has({ role: 'admin' })) {
    return Response.redirect(new URL('/unauthorized', req.url));
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Regex Pattern:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isApiRoute = createRouteMatcher(/^\/api\/.*/);
const isProtectedPage = createRouteMatcher([
  /^\/dashboard/,
  /^\/profile/,
  /^\/settings/,
]);

export default clerkMiddleware(async (auth, req) => {
  if (isApiRoute(req) || isProtectedPage(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Custom Function:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher((req) => {
  const path = req.nextUrl.pathname;

  // Custom logic to determine if route is protected
  if (path.startsWith('/public')) return false;
  if (path.startsWith('/api/webhook')) return false;
  if (path === '/') return false;

  return true;
});

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Next.js Typed Routes:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher([
  '/dashboard/:path*',
  '/api/users/:userId',
  '/org/:orgId/settings',
]);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Middleware Auth Object

The auth function passed to the middleware handler provides access to authentication state and methods.

/**
 * Auth function in middleware
 * Call to get authentication state
 */
const authObject = await auth();

// Available properties and methods:
authObject.userId;              // Current user ID
authObject.sessionId;           // Current session ID
authObject.orgId;               // Active organization ID
authObject.orgRole;             // User's role in org
authObject.orgSlug;             // Organization slug
authObject.orgPermissions;      // User's permissions
authObject.redirectToSignIn();  // Redirect to sign-in
authObject.redirectToSignUp();  // Redirect to sign-up
authObject.has({ role });       // Check role
authObject.has({ permission }); // Check permission

Usage Example - Check Auth State:

import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionId, orgId } = await auth();

  console.log('Middleware auth state:', {
    userId,
    sessionId,
    orgId,
    path: req.nextUrl.pathname,
  });
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Get Token:

import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId, getToken } = await auth();

  if (userId) {
    const token = await getToken();
    console.log('Session token:', token);

    // Get custom JWT template
    const customToken = await getToken({ template: 'custom' });
    console.log('Custom token:', customToken);
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Middleware Protect Method

Use auth.protect() in middleware for authentication and authorization checks.

/**
 * Protect routes in middleware
 * Throws error that middleware catches and handles
 */
await auth.protect();                              // Require authentication
await auth.protect({ role: 'admin' });            // Require role
await auth.protect({ permission: 'org:manage' }); // Require permission
await auth.protect((has) => {                     // Custom condition
  return has({ role: 'admin' }) || has({ role: 'moderator' });
});

Usage Example - Basic Protection:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Role-Based Protection:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isAdminRoute = createRouteMatcher(['/admin(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isAdminRoute(req)) {
    await auth.protect({ role: 'admin' });
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Usage Example - Permission-Based Protection:

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isOrgManagementRoute = createRouteMatcher(['/org/*/manage(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isOrgManagementRoute(req)) {
    await auth.protect({ permission: 'org:settings:manage' });
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Advanced Patterns

Pattern 1: Multi-Tier Protection

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher(['/', '/about', '/contact']);
const isAuthRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']);
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
const isOrgRoute = createRouteMatcher(['/org/*/(.*)']);

export default clerkMiddleware(async (auth, req) => {
  const { userId, orgId } = await auth();

  // Public routes - allow everyone
  if (isPublicRoute(req)) {
    return;
  }

  // Auth routes - redirect if signed in
  if (isAuthRoute(req) && userId) {
    return Response.redirect(new URL('/dashboard', req.url));
  }

  // Protected routes - require auth
  if (!isAuthRoute(req) && !userId) {
    return auth().redirectToSignIn();
  }

  // Admin routes - require admin role
  if (isAdminRoute(req)) {
    await auth.protect({ role: 'admin' });
  }

  // Organization routes - require active org
  if (isOrgRoute(req) && !orgId) {
    return Response.redirect(new URL('/select-org', req.url));
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Pattern 2: Custom Headers

import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId, orgId } = await auth();

  const response = NextResponse.next();

  // Add custom headers
  if (userId) {
    response.headers.set('X-User-Id', userId);
  }

  if (orgId) {
    response.headers.set('X-Org-Id', orgId);
  }

  return response;
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Pattern 3: Request Logging

import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionId } = await auth();
  const start = Date.now();

  console.log('Request:', {
    method: req.method,
    path: req.nextUrl.pathname,
    userId,
    sessionId,
    timestamp: new Date().toISOString(),
  });

  const response = await auth.protect();

  const duration = Date.now() - start;
  console.log('Response:', {
    duration: `${duration}ms`,
    status: response?.status || 200,
  });

  return response;
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Pattern 4: Rate Limiting by User

import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

const rateLimitMap = new Map<string, number[]>();

export default clerkMiddleware(async (auth, req) => {
  const { userId } = await auth();

  if (userId && req.nextUrl.pathname.startsWith('/api')) {
    const now = Date.now();
    const userRequests = rateLimitMap.get(userId) || [];

    // Keep only requests from last minute
    const recentRequests = userRequests.filter(time => now - time < 60000);

    if (recentRequests.length >= 100) {
      return NextResponse.json(
        { error: 'Rate limit exceeded' },
        { status: 429 }
      );
    }

    recentRequests.push(now);
    rateLimitMap.set(userId, recentRequests);
  }
});

export const config = {
  matcher: ['/api(.*)'],
};

Matcher Configuration

The matcher config controls which routes the middleware applies to:

/**
 * Matcher configuration
 * Controls which routes middleware applies to
 */
export const config = {
  matcher: [
    // Match all routes except static files and Next.js internals
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',

    // Match root path
    '/',

    // Match all API routes
    '/(api|trpc)(.*)',
  ],
};

Requirements

clerkMiddleware requires:

  1. Environment variables properly set (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY)
  2. ClerkProvider wrapping your application
  3. middleware.ts file in project root (or src/ directory)
# Required environment variables
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxx