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.
Prerequisites
- A working Plainform installation
- Clerk authentication configured
- Basic understanding of Next.js middleware
Plainform includes pre-configured middleware for route protection. This guide shows you how to customize it for your needs.
Steps
Understand Current Protection
Plainform's proxy.ts already protects routes. Review the current configuration:
Note: In Next.js 16+, the middleware file is named proxy.ts instead of middleware.ts.
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)(.*)',
],
};How it works:
- All routes are protected by default
- Routes in
isPublicRouteare accessible without authentication auth.protect()redirects unauthenticated users to sign-in
Add Public Routes
To make a route publicly accessible, add it to isPublicRoute:
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/forgot-password(.*)',
'/sso-callback(.*)',
'/blog(.*)',
'/docs(.*)',
'/api/webhooks(.*)',
'/pricing', // Add this
'/about', // Add this
'/contact', // Add this
'/api/public(.*)', // Add this (all /api/public/* routes)
]);Pattern matching:
/pricing- Exact match/blog(.*)- Matches/blogand all sub-routes like/blog/post-1/api/public(.*)- Matches all routes starting with/api/public/
Protect Specific Routes
Create route matchers for different protection levels:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/blog(.*)',
'/docs(.*)',
]);
const isAdminRoute = createRouteMatcher([
'/admin(.*)',
]);
const isDashboardRoute = createRouteMatcher([
'/dashboard(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
// Protect all non-public routes
if (!isPublicRoute(req)) {
await auth.protect();
}
// Additional check for admin routes
if (isAdminRoute(req)) {
await auth.protect((has) => {
return has({ role: 'org:admin' });
});
}
// Additional check for dashboard routes
if (isDashboardRoute(req)) {
await auth.protect((has) => {
return has({ permission: 'org:dashboard:access' });
});
}
});Add Server-Side Protection
For additional security, add checks in Server Components:
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');
}
// Page content
return <div>Dashboard</div>;
}For 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) {
redirect('/sign-in');
}
if (!has({ role: 'org:admin' })) {
redirect('/'); // Or show access denied page
}
return <div>Admin Dashboard</div>;
}Protect API Routes
Add authentication checks to API routes:
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return new NextResponse('Unauthorized', { status: 401 });
}
const profile = await getUserProfile(userId);
return NextResponse.json(profile);
}
export async function PUT(req: Request) {
const { userId } = await auth();
if (!userId) {
return new NextResponse('Unauthorized', { status: 401 });
}
const data = await req.json();
await updateUserProfile(userId, data);
return NextResponse.json({ success: true });
}For role-based API protection:
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId, has } = await auth();
if (!userId) {
return new NextResponse('Unauthorized', { status: 401 });
}
if (!has({ role: 'org:admin' })) {
return new NextResponse('Forbidden', { status: 403 });
}
const users = await getAllUsers();
return NextResponse.json(users);
}Test Route Protection
Test your route protection:
- Sign out of your application
- Try accessing protected routes - You should be redirected to
/sign-in - Sign in with a regular user account
- Try accessing admin routes - You should see access denied or be redirected
- Sign in with an admin account
- Verify admin routes are accessible
Advanced Protection Patterns
Redirect Authenticated Users
Redirect signed-in users away from auth pages:
const isAuthRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/forgot-password(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
const { userId } = await auth();
// Redirect authenticated users away from auth pages
if (userId && isAuthRoute(req)) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
// Protect non-public routes
if (!isPublicRoute(req)) {
await auth.protect();
}
});Protect Based on Subscription
const isPremiumRoute = createRouteMatcher(['/premium(.*)']);
export default clerkMiddleware(async (auth, req) => {
const { userId } = await auth();
if (!isPublicRoute(req)) {
await auth.protect();
}
if (isPremiumRoute(req) && userId) {
const user = await getUserFromDatabase(userId);
if (!user.isPremium) {
return NextResponse.redirect(new URL('/pricing', req.url));
}
}
});Protect Server Actions
'use server';
import { auth } from '@clerk/nextjs/server';
export async function updateProfile(formData: FormData) {
const { userId } = await auth();
if (!userId) {
throw new Error('Unauthorized');
}
const name = formData.get('name') as string;
await updateUserInDatabase(userId, { name });
revalidatePath('/profile');
return { success: true };
}Client-Side Protection
Conditional Rendering
Hide UI elements 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
- Check that sign-in/sign-up routes are in
isPublicRoute - Verify middleware isn't protecting auth pages
- Ensure
sso-callbackroute is public
Protected Route Still Accessible
- Verify middleware configuration is correct
- Check that
auth.protect()is called for the route - Clear browser cache and cookies
- Restart development server
Middleware Not Running
- Verify
proxy.tsis in the project root - Check the
config.matcherincludes your routes - Ensure file is named exactly
proxy.ts(notproxy.tsx)
Role Check Failing
- Verify user has the correct role in Clerk Dashboard
- Check role name matches exactly (case-sensitive)
- Ensure organizations are enabled if using org roles
- Confirm user is a member of an organization
Best Practices
- Defense in Depth: Use both middleware and server-side checks
- Public by Exception: Protect all routes by default, make specific routes public
- Clear Error Messages: Show helpful messages when access is denied
- Audit Logging: Log access attempts to protected routes
- Test Thoroughly: Test with different user roles and authentication states
- Graceful Degradation: Show appropriate UI when users lack permissions
Next Steps
- Implement Roles - Add role-based access control
- Customize Sign-In - Customize authentication UI
- Add OAuth - Enable social login
Related Documentation
- Authentication Overview - Learn about Clerk integration
- Setup & Configuration - Configure Clerk
- Troubleshooting - Fix common issues
How is this guide ?
Last updated on