Skip to main content

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:

  1. Server Layout calls getMe() which returns permissions: string[] for the active account
  2. PermissionProvider (React Context) makes permissions available to all client components
  3. 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:

  1. Add constant in crates/lo-auth/src/rbac.rs (in pub mod account {})
  2. Update default roles in rbac.rs (ROLE_OWNER, ROLE_MODERATOR, etc.)
  3. Add to UI list in apps/api/src/graphql/roles.rs (get_all_account_permissions())
  4. Write DB migration to add the permission to existing roles
  5. Add GraphQL guard -- #[graphql(guard = "lo_graphql::PermissionGuard::new(\"perm:name\")")]
  6. Add REST guard -- auth.require_permission("perm:name").map_err(...)?;
  7. Add #[utoipa::path] description noting the required permission
  8. Add sidebar mapping in apps/web/src/app/(app)/shell.tsx
  9. Wrap page with <PermissionErrorBoundary permission="perm:read">
  10. Gate write controls with <Gate permission="perm:write">
  11. Update docs in apps/docs/docs/api-reference/permissions.md
  12. 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

  1. Server component fetches permissions:
    • Popout token: GET /v1/tokens/me?token=xxx via serverFetch()
    • Cookie auth: getMe(config).permissions
  2. Client wrapper wraps content in <PermissionProvider> + <ToastProvider>
  3. Components use <Gate> and useHasPerm() — same as dashboard
  4. Hooks pass popout token via withToken() to all API calls

Key Files

FilePurpose
apps/web/src/contexts/permission-context.tsxPermissionProvider + hooks
apps/web/src/contexts/toast-context.tsxToast system
apps/web/src/components/gate.tsxDeclarative permission gate
apps/web/src/components/permission-error-boundary.tsxPage-level access denial
apps/web/src/app/(app)/shell.tsxSidebar permission filtering
crates/lo-auth/src/rbac.rsPermission constants + default roles
apps/api/src/graphql/roles.rsPermission UI list