Role-based Access

Roles and Permissions

Do not scatter role === 'admin' checks everywhere. Define permissions once, check them through one function, and also check ownership (a customer may see only their own orders).

Example

type Role = 'customer' | 'seller' | 'admin';
type Permission = 'product:create' | 'product:update' | 'order:read' | 'order:refund' | 'user:ban';

const permissions: Record<Role, Permission[]> = {
  customer: ['order:read'],
  seller: ['order:read', 'product:create', 'product:update'],
  admin: ['order:read', 'order:refund', 'product:create', 'product:update', 'user:ban'],
};

type User = { id: string; role: Role };
type Order = { id: string; userId: string };

function can(user: User, perm: Permission): boolean {
  return permissions[user.role].includes(perm);
}

function canReadOrder(user: User, order: Order): boolean {
  return can(user, 'order:read') && (user.role === 'admin' || order.userId === user.id);
}

const priya: User = { id: 'u1', role: 'customer' };
const admin: User = { id: 'u9', role: 'admin' };
const order: Order = { id: 'ORD-1001', userId: 'u1' };

console.log('priya reads own order    :', canReadOrder(priya, order));
console.log('priya reads other order  :', canReadOrder(priya, { id: 'ORD-2002', userId: 'u2' }));
console.log('priya refunds            :', can(priya, 'order:refund'));
console.log('admin refunds            :', can(admin, 'order:refund'));

Output

priya reads own order    : true
priya reads other order  : false
priya refunds            : false
admin refunds            : true

A Reusable Guard

// lib/authz.ts
import 'server-only';
import { forbidden, unauthorized } from 'next/navigation'; // experimental authInterrupts in 15/16
import { auth } from '@/auth';

export async function requireRole(...roles: Array<'customer' | 'seller' | 'admin'>) {
  const session = await auth();
  if (!session?.user) unauthorized(); // renders app/unauthorized.tsx (401)
  if (!roles.includes(session.user.role)) forbidden(); // renders app/forbidden.tsx (403)
  return session.user;
}

// usage in a page or action
// const admin = await requireRole('admin');

Tip: If you do not enable authInterrupts, use redirect('/login') and notFound() instead of unauthorized()/forbidden().