We use tracking cookies to understand how you use the product and help us improve it. For more information on how we store cookies, read our  privacy policy.

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.

proxy.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 isPublicRoute are accessible without authentication
  • auth.protect() redirects unauthenticated users to sign-in

Add Public Routes

To make a route publicly accessible, add it to isPublicRoute:

proxy.ts
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 /blog and 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:

proxy.ts
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:

app/dashboard/page.tsx
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:

app/admin/page.tsx
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:

app/api/user/profile/route.ts
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:

app/api/admin/users/route.ts
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:

  1. Sign out of your application
  2. Try accessing protected routes - You should be redirected to /sign-in
  3. Sign in with a regular user account
  4. Try accessing admin routes - You should see access denied or be redirected
  5. Sign in with an admin account
  6. Verify admin routes are accessible

Advanced Protection Patterns

Redirect Authenticated Users

Redirect signed-in users away from auth pages:

proxy.ts
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

proxy.ts
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

actions/updateProfile.ts
'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:

components/ProtectedButton.tsx
'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-callback route 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.ts is in the project root
  • Check the config.matcher includes your routes
  • Ensure file is named exactly proxy.ts (not proxy.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

How is this guide ?

Last updated on