When building standard consumer applications, handling user permissions is usually straightforward: you have a standard user, an admin, and maybe a moderator. But the moment you step into the enterprise world, authorization logic becomes one of the most complex architectural hurdles you will face.
If you are tasked with building a dashboard for a large organization, a simple isAdmin boolean in your database will no longer cut it. Let's dive into how to properly architect Role-Based Access Control (RBAC) for scale on the frontend.
The Real-World Challenge
Imagine you are developing a workforce management system. In massive internal portals—similar to the retail scheduling and payroll networks running over at mysainsbury—the system has to dynamically render different UIs for cashiers, store managers, regional directors, and HR payroll staff.
If you hardcode these roles directly into your UI components, your codebase will quickly become a nightmare of nested if/else statements.
The Solution: Decoupling Roles from Permissions
The biggest mistake developers make is checking against Roles instead of Permissions.
❌ Bad Approach (Role-Based Check):
{user.role === 'store_manager' && <ApproveShiftButton />}
What happens when a 'regional_director' also needs to approve shifts? You have to refactor every single component.
✅ Good Approach (Permission-Based Check):
Instead, map roles to specific permissions at your application's entry point, and let the UI check for the permission.
// permissions.js
export const ROLES = {
STAFF: ['view_schedule', 'view_payslip'],
MANAGER: ['view_schedule', 'edit_schedule', 'approve_shifts'],
HR: ['view_payroll', 'edit_benefits']
};
export const hasPermission = (userRole, action) => {
return ROLES[userRole]?.includes(action) || false;
};
Now, your UI component becomes beautifully clean and scalable:
{hasPermission(user.role, 'approve_shifts') && <ApproveShiftButton />}
Securing the Routes
Frontend route protection is just for UX; the real security always happens on your backend API. However, failing to protect routes on the frontend leads to a clunky user experience.
Instead of wrapping every single page component in a Higher Order Component (HOC), utilize a centralized layout wrapper:
const ProtectedRoute = ({ requiredPermission, children }) => {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
if (!hasPermission(user.role, requiredPermission)) {
return <UnauthorizedView />;
}
return children;
};
Final Thoughts
Enterprise UI architecture is all about planning for scale. By decoupling your UI from hardcoded roles and relying entirely on a strict permissions matrix, you ensure that when the business logic inevitably changes, your frontend codebase won't need a massive rewrite.
I'd love to hear your thoughts!
How do you handle complex RBAC in your current projects? Do you prefer managing permissions on the client, or completely driving the UI state from the backend API? Leave your questions or approaches in the comments below!
Top comments (0)