RBAC & Permissions
Lumio uses a role-based access control (RBAC) system with granular permissions. Every authenticated action is checked against the user's assigned role for the active account.
Default Roles
Every new account is created with four default roles:
| Role | Slug | Color | is_system | is_default | Notes |
|---|---|---|---|---|---|
| Owner | owner | #f59e0b | true | true | All permissions. Cannot be edited or deleted. |
| Administrator | administrator | #ef4444 | false | true | All permissions except account:delete and plan:edit. |
| Moderator | moderator | #22c55e | false | true | Chat moderation, event monitoring, read access. |
| Viewer | viewer | #6b7280 | false | true | Read-only: events:read, overlays:read, events:userinfo. |
Role Properties
| Property | Type | Description |
|---|---|---|
slug | string | Machine-readable identifier. Stable, lowercase, used in code checks. Never changes. |
name | string | Human-readable display name. Editable on custom roles, translated for default roles. |
is_system | bool | Cannot be edited or deleted. Only the Owner role. |
is_default | bool | Cannot be deleted. All four default roles have this set. |
color | string | Hex color for UI display (e.g., "#f59e0b"). |
Role Name Translation
Default role names are translated via the useTranslateRole() hook in apps/web/src/lib/translate-role.ts. The hook returns a (slug, name) => string function that checks if the slug matches a known default role and returns the localized name; custom roles fall back to their name field.
Translation keys live in apps/web/messages/{en,de}.json under "roles.labels":
{
"roles": {
"labels": {
"owner": "Owner",
"administrator": "Administrator",
"moderator": "Moderator",
"viewer": "Viewer"
}
}
}
Permission Format
All permissions follow the resource:action format. Examples:
events:read,events:create,events:deletechat:ban,chat:timeout,chat:notesautomations:execute,automations:history
Rules
- Always granular. Every resource needs at minimum
readplus granularcreate/edit/delete. - Domain-specific actions are added when CRUD is insufficient (e.g.,
automations:execute,chat:ban). - Never use wildcards. No
resource:manageorresource:*for account-level permissions. The only wildcard is the global admin permissionadmin:*, which is restricted to system administrators.
Account Permissions
The tables below summarize the major account permission categories. The authoritative list of constants lives in crates/lo-auth/src/rbac.rs (under pub mod account {}) — treat that file as the source of truth; categories and counts may shift as features evolve.
Events
| Permission | Label |
|---|---|
events:read | Read Events |
events:create | Create Events |
events:delete | Delete Events |
events:userinfo | Event User Info |
Overlays
| Permission | Label |
|---|---|
overlays:read | Read Overlays |
overlays:create | Create Overlays |
overlays:edit | Edit Overlays |
overlays:delete | Delete Overlays |
Spotify (6)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
spotify:read | Read Spotify | x | x | x | |
spotify:playback | Spotify Playback | x | x | x | |
spotify:queue | Spotify Queue | x | x | x | |
spotify:playlist | Spotify Playlists | x | x | x | |
spotify:device | Spotify Devices | x | x | x | |
spotify:worker | Spotify Worker | x | x |
Chat (10)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
chat:read | Read Chat | x | x | x | |
chat:write | Write Chat | x | x | x | |
chat:userinfo | Chat User Info | x | x | x | |
chat:delete | Delete Chat Messages | x | x | x | |
chat:ban | Ban Chat Users | x | x | x | |
chat:timeout | Timeout Chat Users | x | x | x | |
chat:notes | Chat User Notes | x | x | x | |
chat:raid | Cancel Raids | x | x | x | |
chat:poll | End Polls | x | x | x | |
chat:prediction | End Predictions | x | x | x |
Connections
| Permission | Label |
|---|---|
connections:read | Read Connections |
connections:create | Create Connections |
connections:edit | Edit Connections |
connections:delete | Delete Connections |
Settings (2)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
settings:read | Read Settings | x | x | ||
settings:edit | Edit Settings | x | x |
Members (4)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
members:read | Read Members | x | x | x | |
members:create | Create Invites | x | x | ||
members:edit | Edit Members | x | x | ||
members:delete | Delete Members | x | x |
Roles (3)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
roles:read | Read Roles | x | x | x | |
roles:edit | Edit Roles | x | x | ||
roles:delete | Delete Roles | x | x |
Uploads
| Permission | Label |
|---|---|
uploads:read | Read Uploads |
uploads:create | Create Uploads |
uploads:delete | Delete Uploads |
Rewards
| Permission | Label |
|---|---|
rewards:read | Read Rewards |
rewards:create | Create Rewards |
rewards:edit | Edit Rewards |
rewards:delete | Delete Rewards |
Tokens
| Permission | Label |
|---|---|
tokens:read | Read Tokens |
tokens:create | Create Tokens |
tokens:edit | Edit Tokens |
tokens:delete | Delete Tokens |
Automations (4)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
automations:read | Read Automations | x | x | x | |
automations:write | Write Automations | x | x | ||
automations:execute | Execute Automations | x | x | x | |
automations:history | Automation History | x | x | x |
Account (3)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
account:read | Read Account | x | x | ||
account:edit | Edit Account | x | x | ||
account:delete | Delete Account | x |
Plan (2)
| Permission | Label | Owner | Admin | Mod | Viewer |
|---|---|---|---|---|---|
plan:read | Read Plan | x | x | ||
plan:edit | Edit Plan | x |
Admin-Scope Permissions
Admin-scope permissions gate the Lumio admin panel (/admin) and are entirely separate from account-scope permissions. They are stored in the admin_permissions column of CachedPermissions (not account_permissions) and enforced by require_admin_permission().
Two Modules, Two Contexts
The crates/lo-auth/src/rbac.rs file defines two modules:
pub mod account {}— Account-scope permission constants (listed above)pub mod global {}— Admin-scope permission constants
Constants in global use the same resource:action format. The canonical list is exposed via:
lo_auth::rbac::all_admin_permissions() -> Vec<(&'static str, &'static str)>
// Returns (permission_string, category_label) pairs
This function is used by the permission picker UI and the REST/GraphQL permission validation.
Admin Roles
Admin roles are managed in the admin_roles / admin_role_permissions / user_admin_roles tables (introduced in migration 20260412000001). Unlike account roles, admin roles:
- Are global (not account-scoped)
- Have
is_system: bool— system roles cannot be deleted - Auto-receive
admin:access(the dashboard entry gate) on creation - Are CRUD-managed via
adminRoles*GraphQL mutations andGET/POST/PATCH/DELETE /v1/admin/admin-roles
Shared Permissions
Some permission strings (e.g., copyright:read, obs:read, bot-modules:read) are declared in both pub mod account {} and pub mod global {} with identical string values. This is intentional — the same string gates different resources in different contexts:
- Account scope: enforced by
auth.require_permission(account::COPYRIGHT_READ)in account route handlers - Admin scope: enforced by
auth.require_admin_permission(global::COPYRIGHT_READ)in admin route handlers
This follows the precedent set by bot-connections:*.
admin:access
admin:access is the single gate that controls dashboard access. Any user without this permission sees a "No Access" page. It is:
- Always present in the
globalmodule asadmin::ACCESS - Auto-injected into every admin role at creation time (both REST and GraphQL mutation)
- Never removed by the permission diff algorithm
Backend Enforcement
GraphQL: PermissionGuard
Every GraphQL resolver that requires authorization uses the PermissionGuard from crates/lo-graphql/src/guard.rs:
#[graphql(guard = "lo_graphql::PermissionGuard::new(\"events:read\")")]
async fn events(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<Event>> {
// ...
}
The guard checks AuthContext::has_permission() and returns a GraphQL error ("Missing permission: events:read") if the user lacks the permission.
REST: require_permission
Every Actix route handler uses require_permission() from crates/lo-auth/src/context.rs:
use lo_auth::rbac::account;
pub async fn list_events(auth: Auth, state: web::Data<AppState>) -> Result<HttpResponse, ApiError> {
auth.require_permission(account::EVENTS_READ)
.map_err(from_auth_error)?;
// ...
}
This returns an AuthError::MissingPermission which maps to HTTP 403.
Permission Constants
All permission constants are defined in crates/lo-auth/src/rbac.rs inside pub mod account {}. Use the constants (not string literals) in REST handlers to prevent typos.
Frontend Enforcement
PermissionProvider
The PermissionProvider React context (apps/web/src/contexts/permission-context.tsx) receives the user's permissions from the server layout and makes them available to all client components via hooks.
Gate Component
Declaratively renders children only when the user has the required permission:
<Gate permission="overlays:write">
<Button>Create Overlay</Button>
</Gate>
PermissionErrorBoundary
Wraps entire page content to show a "No Access" fallback for users without the permission:
<PermissionErrorBoundary permission="spotify:read">
<MusicPlayer />
</PermissionErrorBoundary>
Imperative Checks
For use in hooks or event handlers:
const canEdit = useHasPerm("overlays:write");
See Frontend Permission System for the full API reference.
Redis Cache
Permissions are cached in Redis to avoid database lookups on every request.
CachedPermissions
The CachedPermissions struct in crates/lo-cache/src/client.rs stores permissions split by scope:
pub struct CachedPermissions {
pub global: Vec<String>, // Admin-level permissions
pub account: Vec<String>, // Account-level permissions
}
- Cache key:
lumio:perms:{user_id}:{account_id} - TTL: 300 seconds (5 minutes)
- Invalidation:
RedisClient::invalidate_permissions()is called whenever a user's role assignment changes
Key Files
| File | Purpose |
|---|---|
crates/lo-auth/src/rbac.rs | Permission constants (account + global/admin) + all_admin_permissions() |
crates/lo-auth/src/context.rs | AuthContext::has_permission() + require_permission() + require_admin_permission() |
crates/lo-graphql/src/guard.rs | PermissionGuard for GraphQL resolvers |
crates/lo-cache/src/client.rs | CachedPermissions + Redis cache/invalidation |
apps/api/src/graphql/roles.rs | GraphQL available_permissions list (account scope) |
apps/api/src/graphql/admin.rs | GraphQL adminRoles* queries and mutations (admin scope) |
apps/api/src/routes/roles.rs | REST available_permissions list + validation |
apps/api/src/routes/admin.rs | REST admin-roles + admin-permissions endpoints |
apps/api/src/db/admin_roles.rs | DB helpers for admin role CRUD |
apps/web/src/contexts/permission-context.tsx | PermissionProvider + usePermissions() + useHasPerm() |
apps/web/src/components/gate.tsx | <Gate> declarative permission component |
apps/web/src/components/permission-error-boundary.tsx | Page-level access denial |
apps/web/src/lib/translate-role.ts | translateRole() hook for default role names |