Clerk SDK for Next.js providing authentication and user management with support for both App Router and Pages Router architectures
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_xxxxxInstall with Tessl CLI
npx tessl i tessl/npm-clerk--nextjs