Loading..

How to Implement Role-Based Access Control (RBAC) in Scalable Web Systems

As your business grows, the need to manage automatically is increasing rapidly. Plus, all the pages that users access, the check up on the permission becomes increasingly complex. Because you need to know what actions team members can perform there. 

In today’s blog, we’re going to guide you in the process of setting up  a production-ready RBAC system for Next.js type applications that handles everything from navigation filtering to granular button-level permissions.

Traditional system is not catching up anymore with the current needs

Maintenance nightmare: Permission logic is duplicated everywhere

Security errors: Easy to forget permission checks

Poor quality UX: Users see buttons they can’t use

Inflexible: Hard to add new roles or modify permissions of the admins

Step 1:Building the Permission Foundation

First, we need to gather and analyze all possible permissions in your system:

Here, we indicate all the permissions:

// lib/rbac/permissions.ts

export enum Permission {

 // User Management

 CREATE_USER = ‘create_user’,

 READ_USER = ‘read_user’,

 UPDATE_USER = ‘update_user’,

 DELETE_USER = ‘delete_user’,

 

 // Sales Management

 CREATE_LEAD = ‘create_lead’,

 READ_LEAD = ‘read_lead’,

 UPDATE_LEAD = ‘update_lead’,

 DELETE_LEAD = ‘delete_lead’,

 

 // Reports & Analytics

 VIEW_REPORTS = ‘view_reports’,

 VIEW_ANALYTICS = ‘view_analytics’,

 

 // Settings

 MANAGE_COMPANY_SETTINGS = ‘manage_company_settings’,

 MANAGE_BILLING = ‘manage_billing’

And now, we map roles to their permissions:

// lib/rbac/roles.ts

export enum UserRole {

 ADMIN = ‘admin’,

 MANAGER = ‘manager’,

 SALESPERSON = ‘salesperson’

}

 

export const ROLE_PERMISSIONS = {

 [UserRole.ADMIN]: [

   // Admins get all permissions

   …Object.values(Permission)

 ],

 

 [UserRole.MANAGER]: [

   Permission.READ_USER,

   Permission.UPDATE_USER,

   Permission.CREATE_LEAD,

   Permission.READ_LEAD,

   Permission.UPDATE_LEAD,

   Permission.DELETE_LEAD,

   Permission.VIEW_REPORTS,

   Permission.VIEW_ANALYTICS

 ],

 

 [UserRole.SALESPERSON]: [

   Permission.CREATE_LEAD,

   Permission.READ_LEAD,

   Permission.UPDATE_LEAD

 ]

}; 

Step 2. Enabling Smart Navigation Filtering

So, in order not to mess up a navigation system we need to enable automatically hides inaccessible routes:

// lib/navigation/navigation-config.ts

export interface NavigationItem {

 id: string;

 label: string;

 href: string;

 icon: any;

 permission?: Permission;

 children?: NavigationItem[];

 badge?: string;

}

 

export const NAVIGATION_CONFIG: NavigationItem[] = [

 {

   id: ‘dashboard’,

   label: ‘Dashboard’,

   href: ‘/dashboard’,

   icon: Home

 },

 {

   id: ‘sales’,

   label: ‘Sales’,

   href: ‘/dashboard/sales’,

   icon: TrendingUp,

   permission: Permission.READ_LEAD,

   children: [

     {

       id: ‘leads’,

       label: ‘Leads’,

       href: ‘/dashboard/sales/leads’,

       icon: UserPlus,

       permission: Permission.READ_LEAD

     },

     {

       id: ‘reports’,

       label: ‘Sales Reports’,

       href: ‘/dashboard/sales/reports’,

       icon: FileText,

       permission: Permission.VIEW_REPORTS,

       badge: ‘Manager+’

     }

   ]

 },

 {

   id: ‘users’,

   label: ‘User Management’,

   href: ‘/dashboard/users’,

   icon: Users,

   permission: Permission.READ_USER,

   badge: ‘Admin’

 }

]; 

By doing that the filtering logic removes items users can’t access:

// lib/navigation/navigation-utils.ts

export function filterNavigationByRole(

 navigation: NavigationItem[],

 userRole: UserRole

): NavigationItem[] {

 return navigation

   .map(item => {

     // Check if user has permission for this item

     if (item.permission && !hasPermission(userRole, item.permission)) {

       return null;

     }

 

     // Filter children recursively

     if (item.children) {

       const filteredChildren = filterNavigationByRole(item.children, userRole);

      

       // Hide parent if no children are accessible

       if (filteredChildren.length === 0) {

         return null;

       }

 

       return { …item, children: filteredChildren };

     }

 

     return item;

   })

   .filter((item): item is NavigationItem => item !== null);

 

Step 3: Context-Aware Action Permissions

For enabling control, we need action-specific permissions that consider data ownership:

// lib/rbac/action-permissions.ts

export enum ActionPermission {

 DELETE_USER = ‘delete_user’,

 EDIT_USER = ‘edit_user’,

 DELETE_LEAD = ‘delete_lead’,

 EDIT_LEAD = ‘edit_lead’,

 ASSIGN_LEAD = ‘assign_lead’

}

 

export interface ActionContext {

 resourceOwnerId?: string;

 currentUserId: string;

 userRole: UserRole;

}

 

export function hasActionPermission(

 userRole: UserRole,

 action: ActionPermission,

 context?: ActionContext

): boolean {

 const rolePermissions = ROLE_ACTION_PERMISSIONS[userRole] || [];

 

 if (!rolePermissions.includes(action)) {

   return false;

 }

 

 // Salesperson can only edit their own leads

 if (userRole === UserRole.SALESPERSON && context) {

   if ([ActionPermission.EDIT_LEAD].includes(action)) {

     return context.resourceOwnerId === context.currentUserId;

   }

 }

 

 return true;

}

Step 4: Adding Smart UI Components

Now we need frontend sustainability to automatically handle permissions:

// components/common/action-guard.tsx

interface ActionGuardProps {

 action: ActionPermission;

 children: React.ReactNode;

 fallback?: React.ReactNode;

 resourceOwnerId?: string;

}

 

export function ActionGuard({

 action,

 children,

 fallback = null,

 resourceOwnerId

}: ActionGuardProps) {

 const { user } = useAuth();

 

 if (!user) return <>{fallback}</>;

 

 const actionContext: ActionContext = {

   currentUserId: user.id,

   userRole: user.role,

   resourceOwnerId

 };

 

 const canPerformAction = hasActionPermission(user.role, action, actionContext);

 

 return canPerformAction ? <>{children}</> : <>{fallback}</>;

}

And a smart button component:

// components/common/action-button.tsx

export function ActionButton({

 action,

 onClick,

 variant = ‘default’,

 children,

 resourceOwnerId,

 …props

}: ActionButtonProps) {

 return (

   <ActionGuard action={action} resourceOwnerId={resourceOwnerId}>

     <Button variant={variant} onClick={onClick} {…props}>

       {children}

     </Button>

   </ActionGuard>

 );

}

 

Building a robust RBAC system upfront saves countless hours of refactoring later. With Intactdia you can build a RBAC system that will expand naturally as your application grows, whether you’re adding new roles, permissions, or complex business rules.