Building Multi-Role Systems with Clerk 👥
Most apps need more than just "logged in" vs "logged out". You need "Admins" who can delete things and "Users" who can only view them. This is RBAC (Role-Based Access Control).
The Problem I Faced
I needed an admin panel for my SaaS. I considered creating a separate database table for roles, but that required a database lookup on every request, slowing down the app.
The Solution: Clerk Custom Claims
We can attach the user's role directly to their session token (JWT). This means we know if they are an Admin instantly, without hitting the database.
Code Example 1: Assigning Roles (Metadata)
You can assign roles in the Clerk Dashboard or via API.
// Determining access in a component
import { auth } from "@clerk/nextjs";
export default function AdminDashboard() {
const { sessionClaims } = auth();
// Custom metadata set in Clerk
const role = sessionClaims?.metadata?.role;
if (role !== "admin") {
return <p>Access Denied</p>;
}
return <h1>Admin Section</h1>;
}Code Example 2: Middleware Protection
Protect entire routes using Next.js Middleware.
import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export default authMiddleware({
afterAuth(auth, req) {
if (req.nextUrl.pathname.startsWith("/admin")) {
if (auth.sessionClaims?.metadata?.role !== "admin") {
return NextResponse.redirect(new URL("/", req.url));
}
}
}
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};Common Pitfalls
- Mistake 1: Checking roles only on the frontend.
- Don't: Rely on
useUser()hook for security. - Do: Always verify metadata/claims on the server (API/Server Components).
- Don't: Rely on
Pro Tips
- Type Safety: Create a
global.d.tsfile to extend Clerk'sCustomJwtSessionClaimsinterface so you get autocomplete formetadata.role.
Wrapping Up
Using session claims for RBAC is performant and secure. It keeps your authorization logic close to the authentication source.
Tech Stack: Clerk, Next.js, TypeScript
