Role-Based Access Control (RBAC) is a critical security pattern for frontend applications that ensures users only see and interact with features appropriate to their assigned roles, implemented through permission-based rendering and route protection mechanisms.
1. Problem Statement & Requirements
Clarifying questions
- What target devices/browsers does this RBAC system need to support? (e.g., mobile-first, desktop-only, specific browser version requirements)
- What authentication method is being used? (e.g., JWT, OAuth, SAML, session-based)
- Is this a real-time system where role changes should be immediately reflected, or can it work with static data?
- What level of offline support is required? (e.g., can users continue to access resources while offline, or should they be blocked?)
- What accessibility level is required for this RBAC system? (e.g., WCAG 2.1 AA compliance, keyboard navigation support)
- Are there i18n requirements for role names and permissions in multiple languages?
- Is there a design system that RBAC components must integrate with, or should they be built from scratch?
Functional requirements (user-facing)
- Users must only see UI elements and access features they're authorized to use based on their assigned roles
- Role assignments must be configurable through admin UI or API
- Users should receive appropriate error messages when they attempt to access unauthorized resources
- The system must support hierarchical role inheritance (e.g., admin > editor > viewer)
- Role-based navigation should be enforced at the component level, not just UI presentation
- Users must be able to switch between roles if they have multiple role assignments (e.g., in a multi-tenant environment)
- Role permissions should be displayed to users in their profile or settings view
Non-functional requirements
- Performance: RBAC checks must complete in <5ms to avoid UI jank
- Scalability: System should handle 10,000+ users with varying roles efficiently
- Accessibility: All RBAC components must be keyboard navigable and screen reader compatible
- Security: UI-level access control should never replace backend validation
- Maintainability: RBAC logic should be easily extensible for new roles and permissions
Constraints and assumptions
- RBAC data will be fetched from a centralized backend service
- Frontend assumes all role information is already validated and sanitized by the backend
- UI-level access control is a secondary defense mechanism (not the primary security layer)
- Role hierarchy is pre-defined and cannot be dynamically modified by users
- Users can only have one active role at a time (multi-role switching is not required)
- The system assumes that all users have roles assigned at login time
Out of scope
- Role creation, modification, and deletion (admin UI for managing roles)
- Real-time role update propagation to connected users
- Role assignment to groups or teams (user-to-group mapping)
- Audit logging of role access attempts
- Custom permission granularities beyond predefined roles (e.g., "read", "write", "delete")
- Integration with external identity providers for role assignment
- Role expiration or time-based access control
2. Component Architecture
graph TD
A[RBACProvider] --> B[RoleGate]
A --> C[PermissionGate]
A --> D[UserContextProvider]
B --> E[RoleBasedComponent]
C --> F[PermissionBasedComponent]
D --> G[UserContext]
D --> H[AuthContext]
H --> I[ProtectedRoute]
I --> J[DashboardLayout]
J --> K[NavigationMenu]
J --> L[ContentArea]
L --> M[FeatureComponent]
Component Tree Breakdown
Smart Components (Container Components):
RBACProvider- Central auth and role state managementRoleGate- Conditional rendering based on user rolesPermissionGate- Conditional rendering based on user permissionsUserContextProvider- User session and profile managementProtectedRoute- Route-level access control
Dumb Components (Presentational Components):
RoleBasedComponent- UI elements that only render for specific rolesPermissionBasedComponent- UI elements that only render for users with specific permissionsNavigationMenu- Role-aware navigation itemsFeatureComponent- Feature-specific UI that requires permission checks
TypeScript Props Contracts
// RBACProvider props
interface RBACProviderProps {
children: React.ReactNode;
userRoles: string[];
userPermissions: string[];
defaultRole?: string;
}
// RoleGate props
interface RoleGateProps {
allowedRoles: string[];
fallback?: React.ReactNode;
children: React.ReactNode;
}
// PermissionGate props
interface PermissionGateProps {
requiredPermissions: string[];
fallback?: React.ReactNode;
children: React.ReactNode;
}
// ProtectedRoute props
interface ProtectedRouteProps {
allowedRoles?: string[];
requiredPermissions?: string[];
redirectPath?: string;
children: React.ReactNode;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Loading : fetchUser
Loading --> Authorized : userLoaded
Loading --> Unauthorized : authFailed
Authorized --> Idle : logout
Unauthorized --> Idle : retryAuth
Component Responsibilities
RBACProvider
- Manages global RBAC state (user roles, permissions)
- Handles authentication flow and session management
- Provides context to child components
- Implements role-based route protection
RoleGate
- Conditionally renders children based on user roles
- Accepts array of allowed roles and fallback UI
- Implements strict role checking logic
PermissionGate
- Conditionally renders children based on user permissions
- Supports both single and multiple permission checks
- Provides granular access control at component level
ProtectedRoute
- Route-level access control implementation
- Redirects unauthenticated or unauthorized users
- Wraps protected components with role and permission checks
Reusability Decisions
The RBAC system is designed to be highly reusable across features:
- Components are generic and configurable
- Context-based state management enables cross-feature access
- Permission and role definitions are centralized
- Fallback UI patterns support consistent error handling
Component Lifecycle Considerations
- Initialization: RBACProvider fetches user session on mount
- Updates: Role and permission changes trigger re-renders of protected components
- Cleanup: Session cleanup on logout or token expiration
- Conditional Rendering: Components only mount when access is granted
- Fallback Handling: Graceful degradation for unauthorized users
// Example usage of RBAC components in a feature component
const FeatureDashboard = () => {
return (
<RBACProvider userRoles={['admin', 'editor']} userPermissions={['read:dashboard', 'write:dashboard']}>
<ProtectedRoute allowedRoles={['admin']} redirectPath="/unauthorized">
<DashboardLayout>
<NavigationMenu />
<ContentArea>
<RoleGate allowedRoles={['admin']}>
<AdminSettingsPanel />
</RoleGate>
<PermissionGate requiredPermissions={['read:dashboard']}>
<DashboardMetrics />
</PermissionGate>
</ContentArea>
</DashboardLayout>
</ProtectedRoute>
</RBACProvider>
);
};
3. State Management Design
Local state vs. global state decisions
For RBAC, we need to track:
- Current user's roles and permissions (global state)
- Role assignment data (global state)
- UI state for permission-based rendering (local to components)
User roles and permissions should be global since they affect the entire application. Role assignment data is also global as it's shared across admin interfaces and user management flows.
State management approach
Zustand is selected for its simplicity, minimal boilerplate, and excellent TypeScript support. It's ideal for medium-sized applications where Redux Toolkit might be overkill but Context API lacks structure.
Zustand offers:
- Simple store creation with hooks
- Built-in middleware support (e.g., for logging, persistence)
- No provider wrapping required
- Clear separation of concerns between state and logic
State shape and normalization strategy
interface UserRole {
id: string;
name: string;
description?: string;
permissions: string[];
}
interface User {
id: string;
email: string;
name: string;
roles: string[]; // role IDs
}
interface RBACState {
currentUser: User | null;
roles: Record<string, UserRole>;
permissions: Record<string, boolean>; // permission -> hasPermission
loading: boolean;
error: string | null;
}
Server state vs. client state separation
- Server state: User roles, permissions, role assignments (fetched from backend)
- Client state: Current user's permission cache, UI loading states, error messages
Cache invalidation strategy
- Invalidate role/permission cache on user role change
- Invalidate when new roles are assigned to users
- Use optimistic updates for immediate UI feedback during role assignment
Optimistic updates design
When a user's roles are updated:
- Immediately update local state to reflect new roles
- Show updated UI immediately
- If API call fails, rollback to previous state and show error
Critical-path hook implementation
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface UserRole {
id: string;
name: string;
permissions: string[];
}
interface User {
id: string;
email: string;
name: string;
roles: string[];
}
interface RBACState {
currentUser: User | null;
roles: Record<string, UserRole>;
permissions: Record<string, boolean>;
loading: boolean;
error: string | null;
fetchUserPermissions: () => Promise<void>;
assignRoleToUser: (userId: string, roleId: string) => Promise<void>;
hasPermission: (permission: string) => boolean;
hasRole: (roleName: string) => boolean;
}
const useRBACStore = create<RBACState>()(
devtools((set, get) => ({
currentUser: null,
roles: {},
permissions: {},
loading: false,
error: null,
fetchUserPermissions: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/user/permissions');
const data = await response.json();
set({
currentUser: data.user,
roles: data.roles,
permissions: data.permissions,
loading: false
});
} catch (error) {
set({
loading: false,
error: 'Failed to fetch permissions',
permissions: {},
currentUser: null
});
}
},
assignRoleToUser: async (userId, roleId) => {
const currentRoles = get().currentUser?.roles || [];
// Optimistic update
set((state) => ({
currentUser: {
...state.currentUser!,
roles: [...currentRoles, roleId]
},
roles: {
...state.roles,
[roleId]: {
id: roleId,
name: 'temp',
permissions: []
}
}
}));
try {
await fetch(`/api/users/${userId}/roles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roleId })
});
// Refresh permissions after successful assignment
await get().fetchUserPermissions();
} catch (error) {
// Rollback on failure
set((state) => ({
currentUser: {
...state.currentUser!,
roles: currentRoles
}
}));
}
},
hasPermission: (permission) => {
return get().permissions[permission] === true;
},
hasRole: (roleName) => {
const user = get().currentUser;
if (!user) return false;
const roleIds = Object.keys(get().roles);
const matchingRole = roleIds.find(id =>
get().roles[id]?.name === roleName
);
return user.roles.includes(matchingRole || '');
}
}), { name: 'rbac-store' })
);
// Custom hook for permission checking
export const useRBAC = () => {
const {
currentUser,
roles,
permissions,
loading,
error,
fetchUserPermissions,
assignRoleToUser,
hasPermission,
hasRole
} = useRBACStore();
return {
currentUser,
roles,
permissions,
loading,
error,
fetchUserPermissions,
assignRoleToUser,
hasPermission,
hasRole
};
};
// Permission guard component
export const PermissionGuard: React.FC<{
permission: string;
fallback?: React.ReactNode;
children: React.ReactNode;
}> = ({ permission, fallback, children }) => {
const { hasPermission } = useRBAC();
if (!hasPermission(permission)) {
return fallback || null;
}
return <>{children}</>;
};
The hook provides:
hasPermissionfor runtime checkshasRolefor role-based checksassignRoleToUserwith optimistic updatesfetchUserPermissionsfor initial data loading- Built-in error handling and loading states
This design enables efficient permission checking throughout the app while maintaining a clean separation between UI logic and state management.
4. Routing & Navigation
Route Structure and Hierarchy
The RBAC system requires a well-structured routing hierarchy that supports role-based access control. The route structure is organized as follows:
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
C --> D[Admin Routes]
C --> E[User Routes]
C --> F[Guest Routes]
Public Routes:
/login- Authentication page/register- Registration page/forgot-password- Password reset flow
Protected Routes (authenticated users only):
/dashboard- User's main dashboard/profile- User profile management/settings- User preferences and settings
Role-based Protected Routes:
/admin- Admin-only dashboard (requires ADMIN role)/admin/users- User management (requires ADMIN role)/admin/roles- Role management (requires SUPER_ADMIN role)/reports- Analytics dashboard (requires ANALYST role)
Code Splitting Strategy
Route-based code splitting:
/adminand/admin/*routes will be split into separate bundles using React.lazy and Suspense/dashboardand/profilewill be loaded as part of the main bundle since they're frequently accessed/reportswill be split into a separate chunk as it's used less frequently
Justification:
- Reduces initial bundle size for users without admin access
- Improves load times for role-specific features
- Enables lazy loading of heavy admin components
Navigation Guards and Authentication Flows
The system implements a multi-layered navigation guard strategy:
- Authentication Guard: Redirects unauthenticated users to login page
- Authorization Guard: Checks role permissions before allowing route access
- Session Management: Handles token refresh and session expiration
// RouteGuard.tsx
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
const RouteGuard = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const location = useLocation();
const { isAuthenticated, user, token } = useAuthStore();
useEffect(() => {
// Check if user is authenticated
if (!isAuthenticated && location.pathname !== '/login') {
navigate('/login', { replace: true });
return;
}
// Check if user has required permissions
const requiredRoles = getRequiredRoles(location.pathname);
if (requiredRoles && !hasPermission(user?.roles || [], requiredRoles)) {
navigate('/unauthorized', { replace: true });
return;
}
// Handle token refresh if needed
if (token && isTokenExpired(token)) {
refreshAuth();
}
}, [location, isAuthenticated, user]);
return <>{children}</>;
};
Deep Linking Support and URL Parameter Design
URL Structure:
/dashboard?tab=overview- Tab-based navigation/admin/users?page=1&limit=20- Pagination parameters/reports?startDate=2023-01-01&endDate=2023-12-31- Filter parameters
Query Parameter Strategy:
// URL parameter handling for RBAC
const useRBACParams = () => {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
return {
// Role-specific parameters
role: searchParams.get('role') || null,
// Pagination
page: parseInt(searchParams.get('page') || '1'),
limit: parseInt(searchParams.get('limit') || '20'),
// Filters
startDate: searchParams.get('startDate') || null,
endDate: searchParams.get('endDate') || null
};
};
Browser History Management and Scroll Restoration
Scroll Restoration Strategy:
- Uses React Router's built-in scroll restoration for basic cases
- Custom scroll restoration for role-specific dashboard tabs
- Preserves scroll position when navigating between related admin pages
// Custom scroll restoration for role-based dashboards
const useRoleScrollRestoration = () => {
const location = useLocation();
useEffect(() => {
// Restore scroll position for dashboard tabs
if (location.pathname.startsWith('/dashboard')) {
const savedScrollPosition = sessionStorage.getItem(
`scrollPosition_${location.pathname}`
);
if (savedScrollPosition) {
window.scrollTo(0, parseInt(savedScrollPosition));
}
}
}, [location]);
useEffect(() => {
// Save scroll position when leaving dashboard
const handleScroll = () => {
if (location.pathname.startsWith('/dashboard')) {
sessionStorage.setItem(
`scrollPosition_${location.pathname}`,
window.scrollY.toString()
);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [location]);
};
History Management:
- Admin navigation history is preserved separately from user history
- Role-specific navigation breadcrumbs are maintained in session storage
- Back button behavior respects role-based route restrictions
The routing design ensures that users can only access features appropriate to their roles while maintaining a seamless navigation experience across different user personas. The code splitting strategy reduces bundle sizes for users without admin access, and the guard system provides robust security while allowing for flexible role-based access patterns.
5. API Integration Layer
API Client Design Pattern
We'll implement a centralized API client using Axios with request/response interceptors to handle authentication, error handling, and response normalization. The client will be built as a singleton pattern to ensure consistent configuration across the application.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor for adding auth token
this.client.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for handling errors and refresh tokens
this.client.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh token
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
const { data } = await axios.post('/auth/refresh', {
refreshToken,
});
localStorage.setItem('authToken', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return this.client(originalRequest);
}
} catch (refreshError) {
// Redirect to login if refresh fails
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
}
public get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.get<T>(url, config);
}
public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.post<T>(url, data, config);
}
public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, config);
}
public delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, config);
}
}
export const apiClient = new ApiClient();
Request/Response Interceptors
The API client implements interceptors for:
- Authentication: Automatically injects Bearer tokens from localStorage
- Token Refresh: Handles 401 responses by attempting to refresh access tokens
- Error Normalization: Standardizes error responses for consistent handling
- Response Transformation: Converts raw API responses to domain models
Error Handling and Retry Strategy
For RBAC operations, we implement a robust error handling strategy:
// api/errorHandler.ts
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export const handleApiError = (error: any): ApiError => {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
return new ApiError(status, 'Unauthorized access', 'UNAUTHORIZED');
case 403:
return new ApiError(status, 'Access forbidden', 'FORBIDDEN');
case 404:
return new ApiError(status, 'Resource not found', 'NOT_FOUND');
case 429:
return new ApiError(status, 'Rate limit exceeded', 'RATE_LIMITED');
default:
return new ApiError(status, data.message || 'API Error', data.code);
}
} else if (error.request) {
return new ApiError(503, 'Network error - please check your connection', 'NETWORK_ERROR');
} else {
return new ApiError(500, 'Unexpected error occurred', 'INTERNAL_ERROR');
}
};
// Retry strategy for transient errors
export const retryWithBackoff = async (
fn: () => Promise<any>,
retries = 3,
delay = 1000
): Promise<any> => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
}
}
};
Loading, Error, and Empty States Design
RBAC operations require specific state handling:
// hooks/useRolePermissions.ts
import { useState, useEffect } from 'react';
import { apiClient } from '../api/client';
import { handleApiError } from '../api/errorHandler';
interface RolePermission {
id: string;
name: string;
description: string;
permissions: string[];
}
interface RolePermissionsState {
data: RolePermission[] | null;
loading: boolean;
error: ApiError | null;
}
export const useRolePermissions = () => {
const [state, setState] = useState<RolePermissionsState>({
data: null,
loading: false,
error: null,
});
const fetchPermissions = async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await apiClient.get<RolePermission[]>('/roles/permissions');
setState({
data: response.data,
loading: false,
error: null,
});
} catch (error) {
const apiError = handleApiError(error);
setState({
data: null,
loading: false,
error: apiError,
});
}
};
useEffect(() => {
fetchPermissions();
}, []);
return {
...state,
refetch: fetchPermissions,
};
};
Data Fetching Strategy
For RBAC features, we use a hybrid approach:
- Critical routes (user profile, role management) use SSR for initial load
- Dynamic content uses CSR with React Query for caching and optimistic updates
- Static permissions data is cached in localStorage for offline access
API Response Transformation Layer
// api/transformers.ts
import { RolePermission } from '../types';
export const transformRolePermissionResponse = (raw: any): RolePermission => ({
id: raw.id,
name: raw.name,
description: raw.description,
permissions: raw.permissions || [],
});
export const transformRolePermissionsList = (rawList: any[]): RolePermission[] =>
rawList.map(transformRolePermissionResponse);
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant U as User
participant F as Frontend
participant A as API Gateway
participant S as Auth Service
participant R as RBAC Service
U->>F: Request protected route
F->>A: Check auth token
A->>S: Validate token
S-->>A: Token valid/invalid
alt Token valid
A->>R: Fetch user roles and permissions
R-->>A: User role data
A-->>F: Role data
F->>F: Evaluate access control
F->>U: Render appropriate UI
else Token invalid
F->>U: Redirect to login
end
Role-Based Access Control API Endpoints
// api/endpoints.ts
export const RBAC_ENDPOINTS = {
USER_ROLES: '/users/:userId/roles',
ROLE_PERMISSIONS: '/roles/permissions',
USER_PERMISSIONS: '/users/:userId/permissions',
RESOURCE_ACCESS: '/access/check',
ROLE_ASSIGNMENT: '/roles/assign',
PERMISSIONS: '/permissions',
};
Cache Invalidation Strategy
For RBAC operations, we implement cache invalidation on:
- Role assignment changes
- Permission updates
- User role modifications
// api/cache.ts
export const invalidateRBACCache = () => {
// Clear all RBAC-related cache entries
const keysToClear = [
'user-roles',
'role-permissions',
'user-permissions',
'resource-access'
];
keysToClear.forEach(key => {
// Implementation depends on chosen caching strategy (localStorage, React Query, etc.)
});
};
Optimistic Updates Strategy
For role assignment operations, we implement optimistic updates:
// hooks/useRoleAssignment.ts
import { useState } from 'react';
import { apiClient } from '../api/client';
interface RoleAssignment {
userId: string;
roleId: string;
}
export const useRoleAssignment = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const assignRole = async (assignment: RoleAssignment) => {
setLoading(true);
setError(null);
try {
// Optimistically update local state
// This would typically be handled by React Query or similar state management
const response = await apiClient.post('/roles/assign', assignment);
// On success, update cache and local state
return response.data;
} catch (err) {
setError(err as Error);
throw err;
} finally {
setLoading(false);
}
};
return { assignRole, loading, error };
};
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for all user paths
- Interaction to Next Paint (INP): <200ms for interactive elements
- Cumulative Layout Shift (CLS): <0.1 for all user journeys
Bundle Size Budget per Route
- Auth routes: <200KB (including auth libraries, i18n)
- Dashboard route: <400KB (with charts, tables, modals)
- User management route: <350KB (with role assignment UI)
- Settings route: <150KB (lightweight form components)
Critical Rendering Path Optimization
- Critical CSS: Inline critical styles for dashboard and auth pages
- Lazy loading: Non-critical components (charts, modals) loaded on demand
- Resource hints: Preload key fonts and critical assets (e.g.,
rel="preload"for dashboard chart libraries) - Code splitting: Route-based chunking with
React.lazyandSuspense
Image Optimization Strategy
- Formats: WebP for modern browsers, JPEG fallback for legacy
- Lazy loading:
loading="lazy"attribute on all dashboard images - Responsive images:
srcSetwith multiple sizes for dashboard cards - Image compression: 20% reduction in file size without visual degradation
Font Loading Strategy
- Font display:
font-display: swapfor all custom fonts - Preload strategy: Critical font subsets (Latin, Arabic) preloaded via
<link rel="preload"> - Fallback fonts: System fonts as fallback to prevent FOIT
- Font optimization: Subsetting for specific locales (e.g., Arabic font only loads for RTL users)
Memoization Strategy
- React.memo: Apply to all presentational components with stable props (e.g.,
RoleBadge,PermissionCard) - useCallback: Wrap event handlers in parent components (e.g.,
handleRoleChange,handlePermissionToggle) - useMemo: Cache computed role-permission mappings and filtered user lists
- Avoid over-memoization: Do not memoize props that change frequently (e.g., user IDs in role assignment)
Virtualization for Large Lists
- React Window: Implement virtualized lists for user role assignment (e.g., 1000+ users)
- Dynamic item size: Support variable-height items in role assignment modal
- Scroll performance: Track scroll position to prevent unnecessary re-renders
Prefetching and Preloading Strategy
- Route prefetching: Preload dashboard route when user lands on auth page (if they're likely to navigate there)
- API prefetching: Fetch user permissions and roles when auth completes (e.g.,
useQuerywithstaleTime: 5000) - Preload critical assets: Load chart libraries and modals when dashboard is likely to be accessed
- Conditional prefetching: Only prefetch for users with appropriate roles (e.g., admin dashboard only)
// Example: Virtualized User List Component with Performance Optimization
import { FixedSizeList as List } from 'react-window';
interface User {
id: string;
name: string;
role: string;
}
const VirtualizedUserList = ({ users, onRoleChange }: { users: User[]; onRoleChange: (id: string, role: string) => void }) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const user = users[index];
return (
<div style={style} className="user-row">
<span>{user.name}</span>
<select
value={user.role}
onChange={(e) => onRoleChange(user.id, e.target.value)}
className="role-select"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="manager">Manager</option>
</select>
</div>
);
};
return (
<List
height={400}
itemCount={users.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to provide the best user experience while maintaining system stability. The placement follows a hierarchical approach:
- Application-level error boundary - Covers entire app for unhandled errors
- Feature-specific boundaries - Wrap major feature components (e.g., dashboard, user management)
- Component-level boundaries - For critical sub-components (e.g., data tables, forms)
- Route-level boundaries - Handle errors within specific route contexts
graph TD
A[App] --> B[ErrorBoundary]
B --> C[Dashboard]
B --> D[UserManagement]
B --> E[Settings]
C --> F[DashboardLayout]
C --> G[DataTables]
D --> H[UserList]
D --> I[UserForm]
Global Error Handler Design
A centralized error handler manages all uncaught errors and provides consistent feedback:
// ErrorHandlerContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
interface ErrorContextType {
error: Error | null;
setError: (error: Error | null) => void;
clearError: () => void;
}
const ErrorContext = createContext<ErrorContextType | undefined>(undefined);
export const useErrorHandling = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useErrorHandling must be used within ErrorProvider');
}
return context;
};
export const ErrorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const handleError = (event: ErrorEvent) => {
setError(event.error);
};
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
setError(event.reason);
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);
return () => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
};
}, []);
const clearError = () => {
setError(null);
};
return (
<ErrorContext.Provider value={{ error, setError, clearError }}>
{children}
</ErrorContext.Provider>
);
};
Offline / Network Failure Handling
Implement robust offline detection and graceful degradation:
// OfflineHandler.ts
import { useEffect, useState } from 'react';
export const useOfflineDetection = () => {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
};
// OfflineToast.tsx
import React from 'react';
import { Toast } from './Toast';
interface OfflineToastProps {
isOnline: boolean;
}
export const OfflineToast: React.FC<OfflineToastProps> = ({ isOnline }) => {
if (isOnline) return null;
return (
<Toast
type="warning"
message="You are currently offline. Some features may be limited."
autoClose={false}
/>
);
};
Graceful Degradation Patterns
For RBAC-enabled features, implement fallback behaviors:
// RBACFallback.tsx
import React from 'react';
interface RBACFallbackProps {
requiredPermissions: string[];
userPermissions: string[];
fallbackComponent?: React.ReactNode;
}
export const RBACFallback: React.FC<RBACFallbackProps> = ({
requiredPermissions,
userPermissions,
fallbackComponent
}) => {
const hasAccess = requiredPermissions.every(perm =>
userPermissions.includes(perm)
);
if (hasAccess) {
return <>{fallbackComponent}</>;
}
return (
<div className="rbac-fallback">
<p>You don't have permission to access this feature</p>
<p>Required permissions: {requiredPermissions.join(', ')}</p>
</div>
);
};
User Feedback on Errors
Implement comprehensive error feedback mechanisms:
// Toast.tsx
import React, { useEffect } from 'react';
interface ToastProps {
type: 'error' | 'warning' | 'success' | 'info';
message: string;
autoClose?: boolean;
onClose?: () => void;
}
export const Toast: React.FC<ToastProps> = ({
type,
message,
autoClose = true,
onClose
}) => {
const [visible, setVisible] = useState(true);
useEffect(() => {
if (autoClose && type !== 'error') {
const timer = setTimeout(() => {
setVisible(false);
onClose?.();
}, 5000);
return () => clearTimeout(timer);
}
}, [autoClose, type, onClose]);
if (!visible) return null;
return (
<div className={`toast toast--${type}`}>
<span className="toast__message">{message}</span>
{onClose && (
<button
className="toast__close"
onClick={() => {
setVisible(false);
onClose();
}}
>
✕
</button>
)}
</div>
);
};
// ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('RBAC Error Boundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong with RBAC</h2>
<p>Please try refreshing the page or contact support</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.toString()}
</details>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Error Recovery Strategies
Implement automatic recovery for transient errors:
// RetryWithBackoff.tsx
import { useState, useEffect } from 'react';
interface RetryConfig {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
}
export const useRetryWithBackoff = <T>(
asyncFunction: () => Promise<T>,
config: RetryConfig = {}
): {
data: T | null;
loading: boolean;
error: Error | null;
retry: () => void;
} => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
const [retryCount, setRetryCount] = useState<number>(0);
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000
} = config;
const executeWithRetry = async () => {
try {
setLoading(true);
setError(null);
const result = await asyncFunction();
setData(result);
} catch (err) {
if (retryCount < maxRetries) {
const delay = Math.min(
baseDelay * Math.pow(2, retryCount),
maxDelay
);
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, delay);
} else {
setError(err as Error);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
executeWithRetry();
}, [retryCount]);
const retry = () => {
setRetryCount(0);
setData(null);
setError(null);
};
return { data, loading, error, retry };
};
Error Monitoring Integration
Integrate with monitoring systems for production error tracking:
// ErrorReporting.ts
import * as Sentry from '@sentry/react';
export const reportRBACError = (error: Error, context?: any) => {
Sentry.captureException(error, {
contexts: {
rbac: {
...context,
timestamp: new Date().toISOString(),
}
}
});
};
// Usage in RBAC components
try {
// RBAC logic
} catch (error) {
reportRBACError(error, {
userId: currentUser?.id,
requestedPermission: permission,
userRoles: currentUser?.roles
});
}
Resilience Patterns for RBAC Operations
Implement circuit breaker pattern for API calls:
// CircuitBreaker.ts
class CircuitBreaker {
private state: 'closed' | 'open' | 'halfOpen' = 'closed';
private failureCount = 0;
private lastFailureTime: number | null = null;
private timeout: number;
constructor(timeout: number = 5000) {
this.timeout = timeout;
}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
throw new Error('Circuit breaker is OPEN');
}
try {
const result = await fn();
this.reset();
return result;
} catch (error) {
this.recordFailure();
throw error;
}
}
private recordFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= 5) {
this.state = 'open';
setTimeout(() => {
this.state = 'halfOpen';
}, this.timeout);
}
}
private reset() {
this.state = 'closed';
this.failureCount = 0;
this.lastFailureTime = null;
}
}
export const rbacCircuitBreaker = new CircuitBreaker(5000);
8. Testing Strategy
Unit Testing Approach
Component-Level Testing (70% of test coverage)
- Test all presentational components with various prop combinations
- Verify role-based rendering logic for each component
- Test edge cases like missing permissions, loading states, and error states
- Use React Testing Library with Jest for DOM-focused tests
Utility Functions (20% of test coverage)
- Test permission checking logic (
hasPermission,canAccess) - Validate role-to-permission mapping functions
- Test user data transformation utilities
Hook Testing (10% of test coverage)
- Test custom RBAC hooks with mocked user contexts
- Verify permission state updates on user role changes
- Test loading and error state handling in hooks
Integration Testing Strategy
Feature Integration Tests (30% of integration tests)
- Test complete RBAC workflows (user login → permission check → UI rendering)
- Validate navigation guards with different role combinations
- Test component composition in role-based layouts
API Integration Tests (20% of integration tests)
- Verify permission data fetching from backend
- Test role assignment and update propagation
- Validate error handling when permissions are not properly synced
E2E Testing Scope
Cypress Test Coverage (100% of critical user flows)
- User role transition testing (admin → editor → viewer)
- Feature access validation across role boundaries
- Protected route navigation with authentication state changes
- Permission-based UI element visibility testing
Test Data Strategy
- Create role-specific test users with distinct permission sets
- Implement fixtures for each role type (admin, editor, viewer)
- Use environment variables to configure test user credentials
Visual Regression Testing
Screenshot Strategy
- Capture UI state for each role at key feature points
- Test RTL layout variations with different permission sets
- Validate component rendering consistency across browsers
Accessibility Testing Automation
Axe-core Integration
- Run axe-core tests on all role-based UI variations
- Verify keyboard navigation compatibility for each permission level
- Test screen reader support for role-specific content
Test Coverage Targets
Unit Test Coverage: 85%
- Components: 90% coverage
- Utilities: 95% coverage
- Hooks: 80% coverage
Integration Test Coverage: 60%
- Feature flows: 70% coverage
- API interactions: 60% coverage
E2E Test Coverage: 100%
- Critical user journeys: 100% coverage
- Role transition flows: 100% coverage
Visual Regression Coverage: 80%
- Role-specific UI variations: 80% coverage
- RTL and responsive states: 70% coverage
Test Data Management
Test User Configuration
- Define role hierarchy in test configuration files
- Use environment-specific test user credentials
- Implement test data factories for role-based scenarios
Mocking Strategy
- Mock API responses based on user role
- Use MSW (Mock Service Worker) for realistic network behavior
- Implement role-specific mock data sets for each test scenario
Continuous Integration Pipeline
Test Execution Strategy
- Unit tests run on every commit
- Integration tests run on feature branches
- E2E tests run in CI pipeline with parallel execution
- Visual regression tests run on PRs with UI changes
Test Reporting
- Generate coverage reports for each test type
- Integrate test results with CI dashboard
- Implement failure alerts for RBAC-related test failures
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The RBAC system must meet WCAG 2.1 AA standards across all user-facing components:
- Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
- Keyboard Navigation: Full keyboard accessibility for all interactive elements (Tab, Enter, Space, Arrow keys)
- Focus Management: Proper focus indicators and focus trapping where appropriate
- Screen Reader Support: ARIA labels and roles for dynamic content changes
- Error Handling: Clear error messages with proper semantic HTML and ARIA attributes
Keyboard navigation support
All RBAC UI components must be fully navigable via keyboard:
// Example: Keyboard-accessible RoleSelector component
const RoleSelector = ({ roles, selectedRole, onSelect }: RoleSelectorProps) => {
const [focusedIndex, setFocusedIndex] = useState(-1);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedIndex(prev => (prev + 1) % roles.length);
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex(prev => (prev - 1 + roles.length) % roles.length);
break;
case 'Enter':
case ' ':
e.preventDefault();
if (focusedIndex >= 0) {
onSelect(roles[focusedIndex]);
}
break;
case 'Escape':
setFocusedIndex(-1);
break;
}
};
return (
<div
role="listbox"
tabIndex={0}
onKeyDown={handleKeyDown}
className="role-selector"
>
{roles.map((role, index) => (
<div
key={role.id}
role="option"
aria-selected={selectedRole?.id === role.id}
tabIndex={focusedIndex === index ? 0 : -1}
className={`role-option ${focusedIndex === index ? 'focused' : ''}`}
onClick={() => onSelect(role)}
>
{role.name}
</div>
))}
</div>
);
};
Focus management strategy
Focus trapping for modals and dropdowns:
// Focus trap implementation for RoleModal
const useFocusTrap = (isOpen: boolean, containerRef: React.RefObject<HTMLElement>) => {
useEffect(() => {
if (!isOpen) return;
const container = containerRef.current;
if (!container) return;
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleFocus = (e: FocusEvent) => {
if (e.target === lastElement && !e.shiftKey) {
firstElement?.focus();
e.preventDefault();
} else if (e.target === firstElement && e.shiftKey) {
lastElement?.focus();
e.preventDefault();
}
};
container.addEventListener('keydown', handleFocus);
return () => {
container.removeEventListener('keydown', handleFocus);
};
}, [isOpen]);
};
ARIA roles and attributes plan
role="button"for interactive elementsaria-expandedfor dropdowns and expandable sectionsaria-selectedfor selected items in listsaria-labelfor icon-only buttonsaria-describedbyfor error messages and help text
Screen reader compatibility
All dynamic content updates must announce changes to screen readers:
// Example: Dynamic role assignment announcement
const RoleAssignmentNotification = ({
user,
newRole,
oldRole
}: { user: User; newRole: Role; oldRole?: Role }) => {
const [announcement, setAnnouncement] = useState('');
useEffect(() => {
if (newRole) {
setAnnouncement(
`Role updated for ${user.name}: ${oldRole?.name || 'None'} → ${newRole.name}`
);
}
}, [newRole, oldRole, user.name]);
return (
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
);
};
Keyboard navigation for role assignment flow
// Complete keyboard navigation support in RoleAssignmentForm
const RoleAssignmentForm = ({ user, availableRoles }: RoleAssignmentProps) => {
const [activeField, setActiveField] = useState<'roleSelect' | 'submit'>('roleSelect');
const [isModalOpen, setIsModalOpen] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab') {
if (activeField === 'roleSelect') {
setActiveField('submit');
} else {
setActiveField('roleSelect');
}
}
};
return (
<div className="role-assignment-form">
<select
role="combobox"
aria-haspopup="listbox"
aria-expanded={isModalOpen}
onKeyDown={handleKeyDown}
className="role-select"
>
<option value="">Select a role</option>
{availableRoles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
</select>
<button
type="submit"
aria-disabled={activeField !== 'submit'}
className="submit-btn"
>
Assign Role
</button>
</div>
);
};
Focus restoration for modals
When role assignment modal closes, focus returns to the trigger button:
// Focus restoration pattern
const RoleModal = ({
isOpen,
onClose,
user
}: { isOpen: boolean; onClose: () => void; user: User }) => {
const triggerButtonRef = useRef<HTMLButtonElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
// Focus first focusable element in modal
const firstFocusable = modalRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as HTMLElement;
firstFocusable?.focus();
} else {
// Restore focus to trigger button
triggerButtonRef.current?.focus();
}
}, [isOpen]);
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="role-modal-title"
>
<h2 id="role-modal-title">Assign Role to {user.name}</h2>
{/* Modal content */}
</div>
);
};
Semantic HTML structure
All RBAC components use proper semantic HTML:
// Semantic structure for role management dashboard
const RoleManagementDashboard = () => {
return (
<main>
<header>
<h1>Role Management</h1>
<nav aria-label="Role management navigation">
<ul>
<li><a href="#roles">Roles</a></li>
<li><a href="#users">Users</a></li>
</ul>
</nav>
</header>
<section aria-labelledby="roles-heading">
<h2 id="roles-heading">Available Roles</h2>
<div role="list">
{roles.map(role => (
<div role="listitem" key={role.id}>
<h3>{role.name}</h3>
<p>{role.description}</p>
</div>
))}
</div>
</section>
<section aria-labelledby="users-heading">
<h2 id="users-heading">User Assignments</h2>
<table aria-label="User role assignments">
<thead>
<tr>
<th>User</th>
<th>Current Role</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.role?.name || 'No role'}</td>
<td><button aria-label={`Edit role for ${user.name}`}>Edit</button></td>
</tr>
))}
</tbody>
</table>
</section>
</main>
);
};
10. Security Considerations
XSS Prevention Strategy
Content Sanitization: All user-generated content must be sanitized before rendering to prevent script injection. Implement a robust HTML sanitizer using libraries like DOMPurify:
// Sanitize user content before rendering
const sanitizeHTML = (rawHTML: string): string => {
return DOMPurify.sanitize(rawHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
});
};
// Usage in component
const UserContent = ({ content }: { content: string }) => (
<div
dangerouslySetInnerHTML={{
__html: sanitizeHTML(content)
}}
/>
);
Input Validation: All form inputs must be validated both client-side and server-side. Use strict validation schemas:
// Input sanitization for form fields
const sanitizeInput = (value: string): string => {
return value.trim().replace(/[<>'"&]/g, (char) => {
const escapeMap: Record<string, string> = {
'<': '<',
'>': '>',
"'": ''',
'"': '"',
'&': '&'
};
return escapeMap[char];
});
};
CSRF Protection
Token Management: Implement automatic CSRF token injection for all authenticated requests:
// CSRF Token Manager
class CSRFTokenManager {
private token: string | null = null;
getCSRFToken(): string | null {
return this.token;
}
setCSRFToken(token: string): void {
this.token = token;
}
// Inject token into outgoing requests
injectToken(config: AxiosRequestConfig): AxiosRequestConfig {
return {
...config,
headers: {
...config.headers,
'X-CSRF-Token': this.token
}
};
}
}
Content Security Policy (CSP) Headers
Frontend CSP Implementation: Configure CSP headers to restrict resource loading:
// CSP Configuration in helmet-style middleware (client-side)
const CSP_POLICY = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
`;
// Apply CSP to HTML document
const applyCSP = () => {
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = CSP_POLICY;
document.head.appendChild(meta);
};
Sensitive Data Handling in Frontend State
Secure State Management: Protect sensitive data in React state:
// Secure state management with sensitive data handling
interface SecureUserState {
// Public user data (safe to expose)
name: string;
email: string;
// Sensitive data (should not be logged or exposed)
permissions: string[];
role: string;
token: never; // Prevent accidental exposure
}
// Secure data access pattern
const useSecureUserState = (): SecureUserState => {
const [userState, setUserState] = useState<SecureUserState>({
name: '',
email: '',
permissions: [],
role: '',
// token field intentionally omitted
});
const setSecureUser = (newUser: Omit<SecureUserState, 'token'>) => {
// Never store tokens in state
setUserState({
...newUser,
token: undefined as any // Type assertion to prevent token exposure
});
};
return userState;
};
Input Sanitization Approach
Comprehensive Input Validation: Implement layered input sanitization:
// Input validation and sanitization utilities
class InputSanitizer {
static sanitizeText(input: string): string {
// Remove potentially dangerous characters
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
static sanitizeURL(input: string): string {
try {
const url = new URL(input);
// Only allow http/https protocols
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
return url.toString();
} catch (e) {
throw new Error('Invalid URL format');
}
}
static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
Security Headers Implementation: Ensure proper security headers are set:
// Security header configuration
const SECURITY_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin'
};
// Apply security headers to response (server-side)
const applySecurityHeaders = (res: Response) => {
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
res.setHeader(key, value);
});
};
Token Security: Implement secure token handling:
// Secure token storage and management
class TokenManager {
private static readonly TOKEN_KEY = 'auth_token';
static storeToken(token: string): void {
// Use secure HTTP-only cookies when possible
if (typeof window !== 'undefined') {
// For client-side storage, use secure session storage
sessionStorage.setItem(this.TOKEN_KEY, token);
}
}
static getToken(): string | null {
if (typeof window !== 'undefined') {
return sessionStorage.getItem(this.TOKEN_KEY);
}
return null;
}
static clearToken(): void {
if (typeof window !== 'undefined') {
sessionStorage.removeItem(this.TOKEN_KEY);
}
}
}
Security Best Practices Summary:
- Never expose authentication tokens in React state or console logs
- Implement comprehensive input sanitization for all user inputs
- Use proper CSP headers to prevent XSS attacks
- Validate and sanitize all API responses before processing
- Implement proper CSRF token handling for authenticated requests
- Never use
dangerouslySetInnerHTMLwith untrusted content - Implement secure session management with proper token expiration handling
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys will follow a hierarchical, descriptive naming convention using dot notation to ensure clarity and maintainability:
{
"rbac": {
"roles": {
"admin": "Administrator",
"editor": "Editor",
"viewer": "Viewer"
},
"permissions": {
"create": "Create",
"read": "Read",
"update": "Update",
"delete": "Delete"
},
"accessControl": {
"title": "Access Control",
"noAccess": "You don't have access to this resource"
}
}
}
RTL Support Considerations
RTL support will be handled through CSS classes and direction properties:
- Use
dir="rtl"on root HTML element when locale is RTL - Implement CSS
:lang()selectors for direction-specific styling - Apply
text-align: rightto relevant text elements in RTL contexts - Use
margin-left/margin-rightswap for layout elements
Date, Number, Currency Formatting Strategy
All date and number formatting will leverage the browser's built-in Intl API:
- Dates:
new Intl.DateTimeFormat(locale, options).format(date) - Numbers:
new Intl.NumberFormat(locale, options).format(number) - Currency:
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
Locale Detection and Switching Strategy
Automatic locale detection will use browser's navigator.language with fallback to en-US. Manual locale switching will be implemented through a dropdown in the header, persisting user preference in localStorage.
// i18n config example
const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
const defaultLocale = 'en-US';
export const detectLocale = (): string => {
const savedLocale = localStorage.getItem('locale');
if (savedLocale && supportedLocales.includes(savedLocale)) return savedLocale;
const browserLocale = navigator.language || defaultLocale;
return supportedLocales.includes(browserLocale) ? browserLocale : defaultLocale;
};
Translation File Organization
Translation files will be organized by locale in a dedicated locales directory:
src/
locales/
en-US.json
fr-FR.json
es-ES.json
ar-SA.json
Each file will contain the full set of keys for that locale, with fallbacks handled by the i18n library.
Language Switcher Component
A language switcher component will be implemented with:
- Dropdown menu showing all supported languages
- Flag icons for visual recognition
- Current language highlighting
- Persistence of user preference
interface LanguageSwitcherProps {
currentLocale: string;
supportedLocales: { code: string; name: string }[];
onLocaleChange: (locale: string) => void;
}
i18n Utility Functions
Custom utility functions will provide type-safe access to translated strings:
// Type-safe translation function
export const t = (key: string, params?: Record<string, any>): string => {
// Implementation would use i18next or similar
return i18n.t(key, params);
};
// Translation with pluralization support
export const tPlural = (key: string, count: number): string => {
return i18n.t(key, { count });
};
i18n Context Provider
An i18n context will be created to manage the current locale and provide translation functions:
interface I18nContextType {
locale: string;
setLocale: (locale: string) => void;
t: (key: string, params?: Record<string, any>) => string;
tPlural: (key: string, count: number) => string;
}
const I18nContext = createContext<I18nContextType>({
locale: 'en-US',
setLocale: () => {},
t: () => '',
tPlural: () => ''
});
Translation Key Management
To prevent key duplication and ensure consistency:
- All translation keys will be extracted and validated using a build-time tool
- Translation key validation will be part of CI pipeline
- Keys will be grouped by feature area (e.g.,
rbac.roles.admin,rbac.permissions.create)
i18n Testing Strategy
Translation files will be tested for:
- Missing keys across locales
- Consistent key naming patterns
- Valid JSON structure
- Proper fallback handling
Accessibility Considerations for i18n
All translated content will:
- Maintain proper semantic structure
- Support screen readers with appropriate ARIA attributes
- Use
langattribute on translated content blocks - Ensure keyboard navigation works in all locales
Performance Impact
i18n implementation will:
- Load translation bundles on demand via dynamic imports
- Cache translations in memory after initial load
- Minimize runtime overhead through pre-processing of translation keys
12. Monitoring & Observability
Real User Monitoring (RUM) metrics to track
For RBAC-enabled frontend applications, the following RUM metrics are critical:
- Authentication success/failure rates: Track login attempts and their outcomes to detect potential security threats or authentication system issues
- Role-based permission check latency: Measure time taken to validate user permissions for specific actions or routes, as this directly impacts user experience
- Feature access denial rates: Monitor how often users are denied access to features they expect to have, which may indicate misconfigured permissions or UX issues
- Permission refresh frequency: Track how often permission sets are refreshed, helping identify potential performance bottlenecks in the RBAC system
- Route access violation counts: Count unauthorized route access attempts to detect potential security bypasses or misconfigured route guards
Error tracking integration (Sentry / Datadog)
The frontend RBAC system should integrate with error tracking platforms using:
// Error tracking integration for RBAC errors
import * as Sentry from '@sentry/react';
class RBACError extends Error {
constructor(message: string, public context?: any) {
super(message);
this.name = 'RBACError';
}
}
// Global error handler for RBAC-related errors
const handleError = (error: Error, context?: any) => {
if (error instanceof RBACError) {
Sentry.captureException(error, {
contexts: {
rbac: context,
},
});
} else {
Sentry.captureException(error);
}
};
// Example usage in permission checking
const checkPermission = (userRole: string, requiredPermission: string) => {
try {
const hasPermission = userPermissions[userRole]?.includes(requiredPermission);
if (!hasPermission) {
throw new RBACError('Permission denied', {
userRole,
requiredPermission,
timestamp: Date.now(),
});
}
return true;
} catch (error) {
handleError(error, { userRole, requiredPermission });
return false;
}
};
Custom events and user journey tracking
Implement custom event tracking to understand RBAC behavior:
// Custom event tracking for RBAC flows
const trackRBACEvent = (eventName: string, properties?: Record<string, any>) => {
// Send to analytics service
if (window.gtag) {
window.gtag('event', eventName, {
...properties,
timestamp: Date.now(),
});
}
// Log to console for debugging in dev
if (process.env.NODE_ENV === 'development') {
console.log(`[RBAC Event] ${eventName}`, properties);
}
};
// Example usage
const handleRoleChange = (oldRole: string, newRole: string) => {
trackRBACEvent('role_change', {
oldRole,
newRole,
userId: getCurrentUserId(),
});
};
Performance monitoring and regression detection
Monitor RBAC performance with:
- Permission check duration histogram: Track median, 95th percentile, and 99th percentile of permission checks
- Cache hit rate: Monitor how often permission data is served from cache vs. fresh API requests
- Memory usage patterns: Track memory consumption of permission state objects over time
- Bundle size impact: Monitor the size of RBAC-related modules to prevent bloat
Integration with existing observability stack
The monitoring strategy should integrate with:
- Application Performance Monitoring (APM) - For end-to-end transaction tracing
- Log aggregation systems - For structured RBAC logs with user context
- Alerting systems - To notify on permission denial spikes or authentication failures
- User session monitoring - To correlate RBAC behavior with user sessions
Dashboard and alerting strategy
Create dashboards to visualize:
- Permission denial rate over time - Alert if >5% of requests are denied
- Authentication success rates - Alert if <95% of login attempts succeed
- API response time for permission checks - Alert if >200ms median response time
- User role distribution - Monitor for unusual role distributions that might indicate system issues
Data retention and compliance considerations
- Retain RBAC-related telemetry for at least 90 days for security auditing
- Implement data anonymization for PII in telemetry data
- Ensure compliance with privacy regulations (GDPR, CCPA) when collecting user role information
- Implement data lifecycle policies for RBAC telemetry to prevent unnecessary data accumulation
Health check endpoints
Implement health checks for RBAC components:
// RBAC health check endpoint
const rbacHealthCheck = async () => {
const startTime = Date.now();
try {
// Test permission checking
const permissionCheckDuration = Date.now() - startTime;
// Test cache validity
const cacheValid = isPermissionCacheValid();
return {
status: cacheValid ? 'healthy' : 'degraded',
metrics: {
permissionCheckDuration,
cacheValid,
timestamp: Date.now(),
},
};
} catch (error) {
return {
status: 'unhealthy',
error: error.message,
timestamp: Date.now(),
};
}
};
This observability approach ensures that RBAC systems can be monitored for performance, security, and user experience issues in production environments. The combination of error tracking, custom event monitoring, and performance metrics provides comprehensive visibility into RBAC behavior across all user interactions.
13. File & Folder Structure
Proposed Directory Structure
/src
/features
/rbac
/components
RoleSelector.tsx
PermissionBadge.tsx
AccessControlProvider.tsx
/hooks
useRolePermissions.ts
useUserRoles.ts
/types
index.ts
/utils
permissionUtils.ts
roleUtils.ts
/context
RBACContext.tsx
index.ts
/shared
/components
/PermissionGuard
/RoleGuard
/hooks
useRBAC.ts
/types
index.ts
/app
/layout
AppLayout.tsx
/routes
ProtectedRoute.tsx
/services
/api
rbacClient.ts
Module Boundaries and Import Rules
- RBAC feature modules should not import directly from other features to maintain encapsulation
- Shared components like
PermissionGuardandRoleGuardcan be imported by any feature - All RBAC-related logic is contained within the
/features/rbacdirectory - Barrel exports are used for clean imports:
import { useRBAC } from 'shared/hooks'
Shared vs Feature-Specific Code Separation
Shared Components:
PermissionGuard- generic permission checking componentRoleGuard- generic role checking component
Feature-Specific Components:
- Role assignment UI components
- Permission management interfaces
- RBAC context provider
This structure enables:
- Clear separation of concerns between RBAC logic and application features
- Reusable permission checking across the application
- Easy maintenance of RBAC-related functionality in one location
- Feature isolation to prevent tight coupling between modules
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React Router v6+
- Justification: Provides robust routing capabilities with nested routes, route-based code splitting, and built-in support for protected routes. Its hook-based API (useNavigate, useParams) aligns well with modern React patterns and enables clean implementation of RBAC route guards.
- Bundle Impact: ~20KB (minified + gzipped)
- Alternatives Considered: Next.js Router - Too heavy for a standalone SPA, and the feature scope doesn't require SSR benefits.
2. React Query v5+
- Justification: Excellent for managing server state with automatic caching, background updates, and optimistic UI patterns. It handles API retries, error boundaries, and provides built-in loading states that are crucial for RBAC workflows where permissions may be fetched asynchronously.
- Bundle Impact: ~25KB (minified + gzipped)
- Alternatives Considered: Redux Toolkit - Overkill for this feature's state needs; simpler to implement and maintain with React Query.
3. Zustand v4+
- Justification: Lightweight global state management solution ideal for storing user roles, permissions, and current session data. It offers a simple API with minimal boilerplate while providing good performance characteristics for frequent reads/writes.
- Bundle Impact: ~3KB (minified + gzipped)
- Alternatives Considered: Context API - While sufficient, it lacks built-in state update optimization and requires more boilerplate for complex state logic.
4. Tailwind CSS v3+
- Justification: Utility-first CSS framework that enables rapid UI development with consistent design tokens. It supports theme customization and can be easily extended to support dark mode or RTL layouts required for internationalization.
- Bundle Impact: ~15KB (minified + gzipped)
- Alternatives Considered: Styled Components - Adds complexity and bundle size overhead, not necessary for a component-driven RBAC system.
5. Axios v1+
- Justification: Robust HTTP client with interceptors, request/response transformation, and error handling capabilities. It integrates seamlessly with React Query for consistent API interaction patterns.
- Bundle Impact: ~20KB (minified + gzipped)
- Alternatives Considered: Fetch API - While native, lacks built-in interceptor support and requires more boilerplate for consistent error handling.
6. React Hook Form v7+
- Justification: Optimized form handling with built-in validation, error management, and performance optimizations. Essential for forms that may be role-specific (e.g., user creation with role selection).
- Bundle Impact: ~15KB (minified + gzipped)
- Alternatives Considered: Formik - More verbose and less performant for this use case.
Alternatives Considered and Rejected
Redux Toolkit
- Rejected: Overkill for RBAC state management. The feature doesn't require complex middleware or extensive reducer logic that Redux Toolkit provides. Zustand offers simpler, more maintainable global state management.
Jotai
- Rejected: While powerful for granular state, it introduces unnecessary complexity for a feature that benefits more from centralized session and permission management.
Emotion
- Rejected: Adds significant bundle size overhead (~30KB) and complexity for styling when Tailwind CSS provides sufficient utility classes.
React Context with useReducer
- Rejected: While functional, it requires more boilerplate for proper performance optimizations and state updates. Zustand provides better ergonomics and built-in memoization.
Licensing and Bundle Size Impact Assessment
| Library | License | Minified Size | Gzipped Size |
|---|---|---|---|
| React Router | MIT | 20KB | 6KB |
| React Query | MIT | 25KB | 7KB |
| Zustand | MIT | 3KB | 1KB |
| Tailwind CSS | MIT | 15KB | 5KB |
| Axios | MIT | 20KB | 6KB |
| React Hook Form | MIT | 15KB | 5KB |
Total Estimated Bundle Size: ~98KB (minified + gzipped)
Budget Consideration: Within typical SPA bundle size expectations for enterprise applications (100-200KB). Bundle size is optimized through tree-shaking and route-based code splitting.
Feature-Specific Library Usage Patterns
Route-Based Code Splitting:
- React Router's
lazyloading is used to split RBAC-protected routes, ensuring only necessary code loads for each user role.
State Management Integration:
- Zustand stores session state (user, roles, permissions) and provides a clean API for permission checks throughout the app.
API Layer Integration:
- Axios is configured with interceptors to inject auth tokens and handle 403 responses by redirecting to unauthorized pages or showing appropriate UI.
Performance Optimization:
- React Query's built-in caching and automatic refetching reduce redundant API calls when permissions change or users navigate between roles.
This selection balances developer experience, bundle size efficiency, and feature completeness for a robust RBAC implementation.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Role hierarchy definition: Should roles support inheritance (e.g., Admin > Editor > Viewer) or flat permissions? This impacts the complexity of permission evaluation logic and whether we need to build a role graph or maintain a flat permission set per role.
- Permission granularity: Should permissions be granular (e.g.,
canEditUser,canDeletePost) or coarse-grained (e.g.,canManageUsers)? Fine-grained permissions provide more flexibility but increase complexity in permission management and UI rendering. - User session lifecycle: How long should user roles remain cached? Should they be refreshed on every route change, or only when a token expires? This affects cache invalidation strategy and real-time permission sync.
Known trade-offs made in this design
- Client-side vs. server-side enforcement: The frontend RBAC implementation provides UI-level access control but does not enforce permissions at the API layer. This trade-off reduces frontend complexity but introduces security risks if not properly implemented on backend.
- Static vs. dynamic permission loading: We're assuming roles and permissions are loaded once at login time, rather than fetching them dynamically on each route or action. This improves performance but introduces a potential stale-roles issue if permissions change mid-session.
- UI rendering vs. component hiding: We're using conditional rendering to hide components based on permissions, rather than completely removing them from the DOM. This provides better debugging but can lead to unnecessary component instantiation in some edge cases.
Risks and mitigations
- Permission drift: If roles are updated on the backend but not reflected in the frontend, users might see stale permissions. Mitigation: Implement a refresh mechanism or token-based permission sync.
- Performance impact of complex permission checks: Complex role hierarchies or many permissions can slow down conditional rendering. Mitigation: Cache permission checks using
useMemoand optimize the permission evaluation algorithm. - Inconsistent UI state: If a user's role changes during a session, the UI might not update correctly. Mitigation: Use React Context or global state to propagate role changes and re-render affected components.
- Overhead of permission checking in deeply nested components: Deep component trees can lead to performance bottlenecks when checking permissions at every level. Mitigation: Use a centralized permission provider with memoized checks to avoid redundant computations.
Technical debt acknowledged
- Hardcoded role names: The current implementation assumes specific role names like
admin,editor, andviewer. This creates tight coupling between frontend and backend role definitions, making it harder to evolve the system without breaking changes. - Limited support for dynamic permissions: The design assumes a fixed set of roles and permissions. If future requirements demand runtime permission adjustments (e.g., role-based feature flags), the current architecture will require significant refactoring.
- No built-in permission caching: While we use React Context for state management, there's no explicit caching layer for permissions. This means every permission check involves a potentially expensive lookup operation, especially in deeply nested component trees.
- No granular permission overrides: The current design doesn't support per-user or per-resource permission overrides. If future requirements demand fine-grained access control (e.g., "user X can only edit posts from team Y"), the current approach would need to be rethought.
- No audit logging: There's no built-in mechanism to log permission checks or access attempts for security monitoring. This is a critical gap that would need to be addressed in production systems.
The design prioritizes developer experience and performance for typical use cases, but acknowledges that more complex RBAC systems may require additional architectural considerations.