Skip to main content
Chapter 5 Authentication and Middleware

Role-Based Access Control Patterns

16 min read Lesson 20 / 28

Implementing RBAC in Next.js 15

Role-Based Access Control (RBAC) restricts what authenticated users can do based on their role. In Next.js, RBAC spans middleware, Server Components, Server Actions, and the UI.

Extending the Session Type

// types/next-auth.d.ts
import 'next-auth';
import 'next-auth/jwt';

declare module 'next-auth' {
    interface User {
        role: 'user' | 'editor' | 'admin';
    }

    interface Session {
        user: User & {
            id: string;
        };
    }
}

declare module 'next-auth/jwt' {
    interface JWT {
        role: 'user' | 'editor' | 'admin';
    }
}

Permission Check Utility

// lib/permissions.ts
type Role = 'user' | 'editor' | 'admin';

const permissions = {
    'posts:create': ['editor', 'admin'] as Role[],
    'posts:delete': ['admin'] as Role[],
    'posts:publish': ['editor', 'admin'] as Role[],
    'users:manage': ['admin'] as Role[],
} as const;

type Permission = keyof typeof permissions;

export function hasPermission(role: Role, permission: Permission): boolean {
    return permissions[permission].includes(role);
}

Using Permissions in Server Components

// app/blog/[slug]/page.tsx
import { auth } from '@/auth';
import { hasPermission } from '@/lib/permissions';

export default async function PostPage({ params }) {
    const { slug } = await params;
    const session = await auth();
    const post = await getPost(slug);
    const userRole = session?.user?.role ?? 'user';

    return (
        <article>
            <h1>{post.title}</h1>
            <div>{post.content}</div>

            {hasPermission(userRole, 'posts:publish') && (
                <PublishButton postId={post.id} published={post.published} />
            )}

            {hasPermission(userRole, 'posts:delete') && (
                <DeleteButton postId={post.id} />
            )}
        </article>
    );
}

Conditional Navigation by Role

// components/navigation.tsx
import { auth } from '@/auth';
import { hasPermission } from '@/lib/permissions';

export default async function Navigation() {
    const session = await auth();
    const role = session?.user?.role ?? 'user';

    return (
        <nav>
            <a href="/">Home</a>
            <a href="/blog">Blog</a>
            {session && <a href="/dashboard">Dashboard</a>}
            {hasPermission(role, 'posts:create') && <a href="/blog/new">New Post</a>}
            {hasPermission(role, 'users:manage') && <a href="/admin">Admin</a>}
        </nav>
    );
}

The combination of middleware for coarse-grained protection and permission checks for fine-grained access control covers the full RBAC spectrum.