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.
Required Setup:
middleware.ts file in project root (or src/ directory)CLERK_SECRET_KEY and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY environment variablesDefault Behaviors:
auth() function in middleware handler is async and must be awaitedauth.protect() throws error that middleware catches (triggers redirect or 404)debug: false by default (set to true for additional logging)contentSecurityPolicy option provided/sign-in and /sign-up (configurable)/ (configurable via afterSignInUrl/afterSignUpUrl)Threading Model:
auth() function in middleware is async and returns Promiseauth() calls in same middleware execution share same session stateLifecycle:
auth.protect() throws error that Next.js middleware catchesEdge Cases:
auth.protect() in middleware: Throws error that triggers redirect/404auth() called multiple times in middleware: Shares same session stateisSatellite, domain, proxyUrl optionscontentSecurityPolicy option providedExceptions:
auth.protect() throws error (never returns normally if not authenticated)redirectToSignIn() and redirectToSignUp() return Response (don't throw)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)(.*)'],
};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 routeUsage 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)(.*)'],
};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 permissionUsage 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)(.*)'],
};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)(.*)'],
};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)(.*)'],
};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)(.*)'],
};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)(.*)'],
};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(.*)'],
};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)(.*)',
],
};clerkMiddleware requires:
# Required environment variables
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxx