Skip to content
techpotions
Next.js · rbac · authorization · middlewareJuly 14, 20266 min read

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.

Cover illustration for “RBAC in Next.js: A Practical How-To”

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.

Text
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:

TypeScript
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:

TypeScript
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:

TypeScript
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.

TSX
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.

Written by
techpotions
All entries
Best RAG Frameworks: What Broke in Production

Got a build in mind? Tell us about it.