How do you implement Role-Based Access Control on the frontend?
Frontend RBAC is UX, not security. Hide unauthorized UI but treat the server as the only authority. Encode permissions as capabilities (`can('edit:post', resource)`), not raw role names, so policies can evolve.
The first principle. Frontend authorization is not security. Anyone with DevTools can flip a "isAdmin" boolean. Every authorization decision must be re-checked on the server. Frontend RBAC is purely about not showing actions a user can't perform — UX.
With that out of the way, the design:
1. Encode capabilities, not roles. can('post:edit', { resource }) survives policy evolution; role === 'admin' doesn't. Roles map to capabilities; check capabilities everywhere.
2. Centralize policy. A single Permissions provider holds the user's resolved capabilities. Components ask via a hook (useCan('post:edit', post)).
3. Two flavors of guard.
- Hide / disable UI for actions the user can't take.
- Route guard for entire pages — redirect or show a "no access" screen.
- Both call the same
can()function.
4. Resource-aware permissions. can('post:edit', post) may depend on ownership, not just role. Encode that in the policy function.
5. Sync with the server. Capabilities should come from the same source the server uses. Best: server returns the capability set on login. Worst: client computes from a role string and drifts from the server.
6. Audit trail and missing-permission UX. When the server says 403, surface a useful message — "You don't have permission to do X. Contact your admin." — not a generic error.
Code
Follow-up questions
- •How do you keep frontend permissions in sync with backend policy?
- •What's the difference between RBAC and ABAC?
- •How do you handle multi-tenant permissions (org-scoped roles)?
Common mistakes
- •Treating frontend checks as security — they aren't.
- •Hard-coding role strings in components instead of capability checks.
- •Forgetting to refresh capabilities when the user's role changes mid-session.
Performance considerations
- •Resolve all capabilities once on login; avoid round trips per render.
Edge cases
- •Optimistic UI shows a button briefly before capabilities load — gate on a `ready` flag or render a skeleton.
- •Role change in a long-lived session — push via WebSocket or re-evaluate on focus.
Real-world examples
- •Notion's permission model (workspace, page, block) is essentially capability-based; same for GitHub teams/repos.