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
type Capability = "post:edit" | "post:delete" | "billing:manage";
function useCan(cap: Capability, resource?: { ownerId?: string }) {
const me = useUser();
if (cap === "post:edit") return me.role === "admin" || resource?.ownerId === me.id;
if (cap === "billing:manage") return me.role === "admin";
return false;
}
function PostMenu({ post }: { post: Post }) {
const canEdit = useCan("post:edit", post);
return (
<Menu>
{canEdit && <MenuItem onClick={edit}>Edit</MenuItem>}
<MenuItem onClick={open}>Open</MenuItem>
</Menu>
);
}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.