Frontend Permission System
This guide explains how the frontend permission system works and how to use it when developing new features.
Architecture
Permissions flow from the backend to every component:
- Server Layout calls
getMe()which returnspermissions: string[]for the active account - PermissionProvider (React Context) makes permissions available to all client components
- Hooks and components use
usePermissions(),useHasPerm(),<Gate>, and<PermissionErrorBoundary>to gate UI elements
API Reference
usePermissions(): string[]
Returns the full array of permissions for the current user's active account.
const permissions = usePermissions();
useHasPerm(permission: string): boolean
Returns whether the current user has a specific permission.
const canEdit = useHasPerm("overlays:write");
<Gate permission="...">
Conditionally renders children only if the user has the specified permission. Renders nothing otherwise.
<Gate permission="overlays:write">
<Button onClick={createOverlay}>Create Overlay</Button>
</Gate>
<PermissionErrorBoundary permission="...">
Renders a "No Access" fallback UI when the user lacks the required permission. Use this to wrap entire page content for direct URL access protection.
<PermissionErrorBoundary permission="spotify:read">
<MusicPlayer />
</PermissionErrorBoundary>
useToast()
Exposes showToast(message, type) for displaying toast notifications. Used for 403 error feedback.
const { showToast } = useToast();
if (res.status === 403) {
showToast(t("permissions.forbidden"), "error");
}
Adding New Permissions
When adding a new permission to the system, follow this checklist:
- Add constant in
crates/lo-auth/src/rbac.rs(inpub mod account {}) - Update default roles in
rbac.rs(ROLE_OWNER,ROLE_MODERATOR, etc.) - Add to UI list in
apps/api/src/graphql/roles.rs(get_all_account_permissions()) - Write DB migration to add the permission to existing roles
- Add GraphQL guard --
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"perm:name\")")] - Add REST guard --
auth.require_permission("perm:name").map_err(...)?; - Add
#[utoipa::path]description noting the required permission - Add sidebar mapping in
apps/web/src/app/(app)/shell.tsx - Wrap page with
<PermissionErrorBoundary permission="perm:read"> - Gate write controls with
<Gate permission="perm:write"> - Update docs in
apps/docs/docs/api-reference/permissions.md - Update docs in
apps/docs/docs/user-guide/roles-and-permissions.md
Popout Pages
Popout pages (music, events, chat) operate outside the dashboard layout and need their own permission handling.
Pattern
- Server component fetches permissions:
- Popout token:
GET /v1/tokens/me?token=xxxviaserverFetch() - Cookie auth:
getMe(config).permissions
- Popout token:
- Client wrapper wraps content in
<PermissionProvider>+<ToastProvider> - Components use
<Gate>anduseHasPerm()— same as dashboard - Hooks pass popout token via
withToken()to all API calls
Key Files
| File | Purpose |
|---|---|
apps/web/src/contexts/permission-context.tsx | PermissionProvider + hooks |
apps/web/src/contexts/toast-context.tsx | Toast system |
apps/web/src/components/gate.tsx | Declarative permission gate |
apps/web/src/components/permission-error-boundary.tsx | Page-level access denial |
apps/web/src/app/(app)/shell.tsx | Sidebar permission filtering |
crates/lo-auth/src/rbac.rs | Permission constants + default roles |
apps/api/src/graphql/roles.rs | Permission UI list |