RBAC in Next.js: A Practical How-To
A hands-on guide to implementing role-based access control in Next.js — from data modeling and middleware to server-side enforcement. Learn from our real-world admin portal build.

Why RBAC Implementation Next.js Matters
Implementing role-based access control in a Next.js application isn’t just about blocking pages—it’s the foundation of any multi-user platform. Whether you’re shipping an admin dashboard, a SaaS product, or a candidate-screening portal like our work on ReadyShortlist, clear role boundaries keep data safe and workflows smooth.
In this guide, you’ll get a battle-tested approach to RBAC implementation Next.js developers can drop into their projects today. We’ll cover:
- A Prisma role-permission model that scales
- Middleware that catches unauthorized requests at the edge
- Server-side enforcement that actually secures your API
- Role-aware UI that guides users without trusting the client
These patterns powered the multi-role admin portal behind ReadyShortlist, where recruiters, admins, and reviewers each needed different visibility into the 72‑hour vetting pipeline. The same strategies work for your app.
RBAC Implementation Next.js: The Data Model
Takeaway: Start with a simple but extensible role-permission model. Enums in your ORM keep it readable and type-safe.
Define roles and permissions directly in your schema. This scalable RBAC system in Next.js uses a similar approach. For most projects, a single Role enum on the User table is enough. When you need finer control, add a Permission enum and a many-to-many relation.
enum Role {
ADMIN
RECRUITER
REVIEWER
}
model User {
id String @id @default(cuid())
email String @unique
name String
role Role @default(RECRUITER)
accounts Account[]
sessions Session[]
}If you expect complex permission matrices (e.g., “edit briefs but only if assigned”), evolve to a Permission model. The advanced RBAC with middleware and Prisma guide walks through this pattern. Keep it simple until you can’t.
Middleware: The Edge-Level Guard
Takeaway: Use Next.js middleware for route-level protection that fires before the page renders. Combined with an auth provider, it stops unauthorized access immediately.
Next.js middleware gives you a single door to check the session and redirect. As the Vercel community thread on RBAC notes, the fullstack nature of Next lets you authorise once and reuse the logic for both pages and API routes.
Pseudo-code for a middleware with NextAuth:
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
export default withAuth(
function middleware(req) {
const role = req.nextauth?.token?.role;
const path = req.nextUrl.pathname;
// Admin routes
if (path.startsWith("/admin") && role !== "ADMIN") {
return NextResponse.redirect(new URL("/403", req.url));
}
// Recruiter can only access /candidates
if (path.startsWith("/candidates") && !["ADMIN", "RECRUITER"].includes(role)) {
return NextResponse.redirect(new URL("/403", req.url));
}
return NextResponse.next();
},
{
callbacks: { authorized: ({ token }) => !!token },
}
);
export const config = { matcher: ["/admin/:path*", "/candidates/:path*"] };The matcher limits middleware to protected paths. Role information should be injected into the JWT by your auth provider. For Clerk, you use organization roles; for NextAuth, enrich the token in the jwt callback.
Server-Side Enforcement: The Real Gatekeeper
Takeaway: Never trust the client. Every data mutation and protected data fetch must re-verify permissions inside API routes or server components.
Middleware checks a claim in the session token, but it cannot access the database. A user’s role may have changed mid-session, or a malicious actor might craft requests that bypass the frontend. The fix: validate again on the server.
Example with NextAuth in an App Router server component:
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function AdminDashboard() {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "ADMIN") {
redirect("/403");
}
return <Dashboard />;
}In an API route handler:
export async function DELETE(req: Request) {
const session = await getServerSession(authOptions);
if (session?.user?.role !== "ADMIN") {
return new Response("Forbidden", { status: 403 });
}
// ...
}For attribute-based checks (e.g., ownership), compare the session user ID with the resource’s creator field. This pattern keeps your RBAC implementation scalable without needing an external authorization service.
Role-Aware UI: What Users See
Takeaway: Show and hide navigation elements based on roles to declutter the interface, but treat it as UX, not security.
Conditional rendering in your layout helps users self-segment, but it’s purely cosmetic—an attacker can still try direct URLs. Use it together with middleware and server checks.
import { useSession } from "next-auth/react";
const Nav = () => {
const { data: session } = useSession();
const role = session?.user?.role;
return (
<nav>
<Link href="/dashboard">Dashboard</Link>
{role === "ADMIN" && <Link href="/admin">Admin Panel</Link>}
{(role === "RECRUITER" || role === "ADMIN") && (
<Link href="/candidates">Candidates</Link>
)}
</nav>
);
};This pattern mirrors what we delivered in the ReadyShortlist portal: admins saw the full vetting funnel, while reviewers only accessed assigned candidates. Again, the real enforcement lives in the routes and server code.
Putting It All Together: Lessons from ReadyShortlist
Takeaway: A real admin portal with multiple roles validates this approach under load, and yours can too.
When we built ReadyShortlist—an end-to-end platform that screens tech talent in 72 hours—the RBAC implementation Next.js demanded was anything but trivial. We had three core roles:
Role | Capabilities |
|---|---|
Admin | Full access to employers, briefs, candidate pipeline, vetting workflow |
Recruiter | View and manage assigned briefs, invite candidates, see screening results |
Reviewer | Access vetted shortlists only, submit feedback per criterion |
We built the entire system on Next.js with middleware that checked roles at the edge, server-side guards in every API route, and a Prisma schema with an enum. The model described above scaled from prototype to the 380-case evaluation suite without breaking.
If you’re building a platform with similarly complex permission needs, we’ve extracted the patterns that work. You don’t need to over-engineer—a clear role model, middleware, and server-side checks cover 95% of cases. For the remaining 5% (row-level ownership, resource-based rules), add ownership checks in your data layer or a lightweight ability library.
Explore our platform engineering services or start a conversation about your project—we’ve already done the hard thinking.
FAQ
How do I implement RBAC in Next.js using NextAuth and Prisma?
Define a Role enum in your Prisma schema, attach it to the User model, and expose it in NextAuth’s session via the jwt and session callbacks. Guard pages with middleware that reads the token’s role, then re-verify in server components or API routes using getServerSession. This gives you both edge-level and server-side enforcement.
Should I rely on Next.js middleware or API route guards for authorization?
Use both. Middleware provides a fast, stateless check perfect for redirecting unauthenticated users or blocking entire route groups by role. However, it cannot access the database or request body, so every mutation and sensitive data fetch must re-validate permissions inside the API route or server component.
How do I handle ownership or attribute-based access in Next.js?
Extend your role check with a simple ownership comparison: inside the API route, compare the authenticated user’s ID to the resource’s ownerId. For more granular, attribute-based rules, you can adopt a permission model with a many-to-many relation in Prisma or use a library like CASL—just keep the enforcement server-side.