Protect Routes
Learn how to protect routes with middleware, server-side checks, and role-based access control
Learn how to protect routes in your Plainform application using Clerk middleware and authentication checks.
Goal
By the end of this recipe, you'll have secured your application routes with authentication and role-based access control.
Important: By default, clerkMiddleware() does not protect any routes. All routes are public unless you explicitly add protection with auth.protect().
Protection Patterns
Protect All Routes Except Public
Protect all routes by default, make specific routes public:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/forgot-password(.*)',
'/sso-callback(.*)',
'/blog(.*)',
'/docs(.*)',
'/api/webhooks(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});
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)(.*)',
],
};Protect Specific Routes Only
Only protect specific routes (e.g., dashboard):
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/profile(.*)',
'/settings(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});Role-Based Protection
Protect routes based on user roles:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/sign-up(.*)']);
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
if (isAdminRoute(req)) {
await auth.protect((has) => {
return has({ role: 'org:admin' });
});
}
});Pattern matching:
/pricing- Exact match/blog(.*)- Matches/blogand all sub-routes/api/public(.*)- Matches all routes starting with/api/public/
Server-Side Protection
Add checks in Server Components for defense in depth:
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
return <div>Dashboard</div>;
}Role-based protection:
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function AdminPage() {
const { userId, has } = await auth();
if (!userId || !has({ role: 'org:admin' })) {
redirect('/');
}
return <div>Admin Dashboard</div>;
}API Route Protection
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const profile = await getUserProfile(userId);
return NextResponse.json(profile);
}Role-based API protection:
export async function GET() {
const { userId, has } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
if (!has({ role: 'org:admin' })) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const users = await getAllUsers();
return NextResponse.json(users);
}Client-Side Protection
Conditionally render UI based on authentication:
'use client';
import { useAuth } from '@clerk/nextjs';
export function ProtectedButton() {
const { isSignedIn, isLoaded, has } = useAuth();
if (!isLoaded) return <div>Loading...</div>;
if (!isSignedIn) return null;
return (
<>
<button>Protected Action</button>
{has({ role: 'org:admin' }) && <button>Admin Action</button>}
</>
);
}Common Issues
Infinite Redirect Loop
- Ensure sign-in/sign-up routes are in
isPublicRoute - Verify
sso-callbackroute is public
Protected Route Still Accessible
- Check that
auth.protect()is called for the route - Clear browser cache and restart dev server
Middleware Not Running
- Verify
proxy.tsis in project root - Check
config.matcherincludes your routes
Role Check Failing
- Verify user has correct role in Clerk Dashboard
- Check role name matches exactly (case-sensitive)
Next Steps
- Implement Roles - Add role-based access control
- Customize Sign-In - Customize authentication UI
- Add OAuth - Enable social login
How is this guide ?
Last updated on