Building Role-Based Access Control in Next.js with Middleware
How to implement granular role-based access control in Next.js 15 using middleware, JWT tokens, and server-side route protection — without any third-party auth library.

Most authentication tutorials stop at login. But real enterprise applications need authorization — who can access what, at what level, and under what conditions. Role-Based Access Control (RBAC) is the industry standard for solving this, and Next.js middleware is the perfect place to enforce it.
(01) ChapterThe difference between authentication and authorization
Authentication answers 'who are you?' — it verifies identity via login credentials, OAuth, or magic links. Authorization answers 'what can you do?' — it checks whether an authenticated user has permission to access a specific resource or perform a specific action.
- 01Authentication: Is this person who they claim to be?
- 02Authorization: Does this person have permission to do this action?
- 03RBAC: Does this person's role include this permission?
- 04ABAC: Do this person's attributes match the access policy?
(02) ChapterDefining roles and permissions
Start by defining your roles and what each role can do. Keep this in a centralized config file — never scatter permission checks across components.
export const ROLES = {
ADMIN: "admin",
MANAGER: "manager",
EMPLOYEE: "employee",
VIEWER: "viewer",
} as const;
export type Role = (typeof ROLES)[keyof typeof ROLES];
export const PERMISSIONS: Record<Role, string[]> = {
admin: ["read", "write", "delete", "manage_users", "view_analytics", "manage_roles"],
manager: ["read", "write", "view_analytics", "manage_team"],
employee: ["read", "write"],
viewer: ["read"],
};
export function hasPermission(role: Role, permission: string): boolean {
return PERMISSIONS[role]?.includes(permission) ?? false;
}(03) ChapterProtecting routes with Next.js middleware
Next.js middleware runs before every request. It's the ideal place to check authentication and authorization without touching individual page components.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "@/lib/auth";
import { hasPermission } from "@/lib/rbac";
const routePermissions: Record<string, string> = {
"/dashboard/analytics": "view_analytics",
"/dashboard/users": "manage_users",
"/dashboard/roles": "manage_roles",
};
export async function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
const user = await verifyToken(token);
if (!user) {
return NextResponse.redirect(new URL("/login", request.url));
}
const requiredPermission = routePermissions[request.nextUrl.pathname];
if (requiredPermission && !hasPermission(user.role, requiredPermission)) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};(04) ChapterServer-side permission checks in components
Middleware protects routes, but you also need permission checks inside components — for example, hiding the 'Delete' button from users who don't have delete permission.
import { cookies } from "next/headers";
import { verifyToken } from "@/lib/auth";
import { hasPermission } from "@/lib/rbac";
export default async function DashboardPage() {
const token = (await cookies()).get("auth-token")?.value;
const user = await verifyToken(token!);
return (
<div>
<h1>Dashboard</h1>
{hasPermission(user.role, "view_analytics") && <AnalyticsPanel />}
{hasPermission(user.role, "manage_users") && <UserManagement />}
</div>
);
}With this architecture, your Next.js app has enterprise-grade access control. Roles are centralized, permissions are composable, and enforcement happens at the edge before any page renders.
End of entry — DoabStudios Studio


