Roles & Permissions
Overview
The Roles & Permissions module implements a granular Role-Based Access Control (RBAC) system. Every account starts with 4 default roles (Owner, Administrator, Moderator, Viewer), and custom roles can be created. The registry defines 121 account-scope permissions across 28 categories, plus 83 admin-scope permissions and 9 user-scope permissions, all using a resource:action naming convention (e.g., chat:read, overlays:edit). Permissions are enforced at the GraphQL layer via PermissionGuard and at the REST layer via Auth::require_permission(). Permission checks are cached in Redis for performance.
CRUD authority is always granular — create / edit / delete, never a lumped resource:write. (chat:write is the one write action in the registry, and it is a verb — "send a chat message" — not a CRUD grant.) Roles also cannot carry wildcards; see Wildcards.
Architecture
Backend
- RBAC Registry (
crates/lo-auth/src/rbac.rs) -- All permission constants organized into three modules:global(admin-scope),account(per-account), anduser(user-scope, cross-account — the Ideas Hub set). Default role definitions with their permission sets. Plan-based limits. This is a single file, not a directory. - Permission Guards --
lo_graphql::PermissionGuardfor GraphQL resolvers,Auth::require_permission()for REST endpoints; both resolve against the active account.AdminPermissionGuard/require_admin_permission()andrequire_user_permission()cover the other two scopes. - Permission Cache -- Permissions are cached in Redis per user/account pair. The cache is invalidated on member role change and member removal via
lo_api::middleware::auth::invalidate_permission_cache(). - Database -- Roles live in
account_roles, their permissions in theaccount_role_permissionsjoin table. Default roles are created on account creation. - WebSocket -- Roles have no channel of their own, but account permissions gate the WebSocket channels:
channel_gate_forincrates/lo-websocket/src/gate.rsmapsevents→events:read,chat→chat:read,spotify→spotify:read,automations→automations:read,history→history:read,ext-storage→extension-store:read,ext-install-logs→bot-modules:read.
Default Roles
Defined as ROLE_OWNER / ROLE_ADMIN / ROLE_MODERATOR / ROLE_VIEWER in rbac.rs.
| Role | Slug | Color | System | Sort | Description |
|---|---|---|---|---|---|
| Owner | owner | #f59e0b | Yes | 0 | Full account access — every account permission, listed explicitly. Cannot be deleted or assigned to another member. |
| Administrator | administrator | #ef4444 | No | 1 | Full access except account deletion and plan changes (enforced via the owner_id check, not by permissions). |
| Moderator | moderator | #22c55e | No | 2 | Chat moderation and event monitoring. |
| Viewer | viewer | #6b7280 | No | 3 | Read-only access to events and overlays: events:read, events:userinfo, overlays:read, bot-commands:read, extension-store:read, extension-store:review, extension-data:read, widgets:read, sounds:read. |
API
GraphQL Queries
| Query | Returns | Permission |
|---|---|---|
roles | [Role!]! | roles:read |
role(id: UUID!) | RoleDetail | roles:read |
availablePermissions | [AccountPermission!]! | roles:read |
myPermissions | [String!]! | AuthGuard |
Role carries permissionCount and memberCount for the list view; RoleDetail carries the full permissions: [String!]! array. AccountPermission is { key, label, category } — the role-editor catalog.
myPermissions returns the permissions of the current auth context: fresh account-role permissions from the database for a JWT user, and exactly the token's own scope for a scoped API key or popout token (never a ["*"] sentinel). Its REST twin is GET /v1/tokens/me.
GraphQL Mutations
| Mutation | Args | Returns | Permission |
|---|---|---|---|
createRole | input: CreateRoleInput! | RoleDetail! | roles:edit |
updateRole | input: UpdateRoleInput! | RoleDetail! | roles:edit |
deleteRole | id: UUID! | RoleMutationResult! | roles:delete |
CreateRoleInput is { name!, description, color, permissions! }; UpdateRoleInput is { id!, name, description, color, permissions }. Neither accepts sortOrder — sort order is assigned by the system.
Role creation derives the slug from the name (lowercased, spaces replaced with -) and assigns a random pastel color when none is supplied.
REST Endpoints
All paths live under /v1/roles. Bodies are snake_case and mirror the GraphQL inputs.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/roles | roles:read | List roles for the account |
GET | /v1/roles/{id} | roles:read | Get a single role with its permissions |
POST | /v1/roles | roles:edit | Create a role (name, description, color, permissions) |
PATCH | /v1/roles/{id} | roles:edit | Update role name, description, color, or permissions |
DELETE | /v1/roles/{id} | roles:delete | Delete a role (system roles cannot be deleted) |
GET | /v1/roles/permissions | roles:read | Catalog of available account permissions (key, label, category) |
Wildcards
createRole and updateRole validate every submitted permission string against the account registry (lo_auth::rbac::account::get_all_account_permissions()), identically on GraphQL and REST, rejecting anything unknown with Invalid permission: <key>. The registry contains no :* keys, so a wildcard can never be persisted into an account role. Wildcard grants (resource:*, *:*) exist only on config-provisioned System keys.
Permissions
Admin-Scope Permissions
83 permissions, exhaustively enumerated by all_admin_permissions() in rbac.rs; the category is derived from the segment before the :. admin:access is only the dashboard-entry gate for the admin app — every element inside is additionally gated on its own resource:action.
| Category | Permissions |
|---|---|
admin | admin:access, admin:privacy-erase |
users | users:read, users:edit, users:delete |
accounts | accounts:read, accounts:edit, accounts:delete, accounts:overrides-read, accounts:overrides-edit |
billing | billing:read, billing:edit |
support | support:read, support:edit |
features | features:read, features:edit |
plans | plans:read, plans:create, plans:edit, plans:delete |
subscriptions | subscriptions:read, subscriptions:edit |
coupons | coupons:read, coupons:create, coupons:edit, coupons:delete |
audit | audit:read |
audit-log | audit-log:read |
platforms | platforms:read, platforms:edit |
providers | providers:read, providers:edit |
feature-flags | feature-flags:read, feature-flags:edit |
oauth-clients | oauth-clients:read, oauth-clients:create, oauth-clients:edit, oauth-clients:delete |
system-keys | system-keys:read, system-keys:create, system-keys:delete |
system-connections | system-connections:read, system-connections:edit, system-connections:delete |
admin-roles | admin-roles:read, admin-roles:create, admin-roles:edit, admin-roles:delete |
user-roles | user-roles:read, user-roles:create, user-roles:edit, user-roles:delete |
bot-commands | bot-commands:read, bot-commands:create, bot-commands:edit, bot-commands:delete |
bot-control | bot-control:read, bot-control:edit |
bot-connections | bot-connections:read, bot-connections:create, bot-connections:delete |
bot-modules | bot-modules:read, bot-modules:edit |
discord-guilds | discord-guilds:read, discord-guilds:delete |
se-tokens | se-tokens:read, se-tokens:delete |
copyright | copyright:read, copyright:edit, copyright:delete |
obs | obs:read, obs:edit |
extension-review | extension-review:read, extension-review:edit |
developer-verification | developer-verification:read, developer-verification:edit |
developer-limits | developer-limits:read, developer-limits:edit |
ideas | ideas:moderate_read, ideas:moderate_status, ideas:moderate_edit, ideas:moderate_delete, ideas:moderate_comment |
Several strings (bot-commands:*, bot-connections:*, bot-modules:*, copyright:*, obs:*, se-tokens:*) are deliberately declared in both the global and account modules: the same action exists at system level and at account level, and the scopes are checked independently.
Account Permissions
123 permissions in 27 categories, sourced from account::get_all_account_permission_infos() — the single source of truth behind the role-editor picker (GET /v1/roles/permissions / availablePermissions) and the create/update validation registry.
| Category | Permissions |
|---|---|
| Events | events:read, events:create, events:userinfo |
| Overlays | overlays:read, overlays:create, overlays:edit, overlays:delete, overlays:access-read, overlays:access-grant, overlays:access-revoke |
| Spotify | spotify:read, spotify:playback, spotify:volume, spotify:queue, spotify:playlist, spotify:device, spotify:worker |
| Chat | chat:read, chat:write, chat:userinfo, chat:delete, chat:ban, chat:timeout, chat:notes, chat:raid, chat:refresh_user, chat:poll, chat:prediction |
| Connections | connections:read, connections:create, connections:edit, connections:delete |
| Settings | settings:read, settings:edit |
| Account | account:read, account:edit, account:delete |
| Plan | plan:read, plan:edit |
| Members | members:read, members:create, members:edit, members:delete |
| Roles | roles:read, roles:edit, roles:delete |
| Login Assignments | login-assignments:read, login-assignments:create, login-assignments:delete |
| Uploads | uploads:read, uploads:create, uploads:delete |
| Rewards | rewards:read, rewards:create, rewards:edit, rewards:delete |
| Tokens | tokens:read, tokens:create, tokens:edit, tokens:delete |
| Automations | automations:read, automations:create, automations:edit, automations:delete, automations:execute |
| OBS | obs:read, obs:edit, obs:delete |
| Copyright | copyright:read, copyright:edit, copyright:delete, copyright:vote, copyright:report, copyright:recommend, copyright:moderate |
| Bot Modules | bot-modules:read, bot-modules:edit |
| Bot Commands | bot-commands:read, bot-commands:create, bot-commands:edit, bot-commands:delete |
| Bot Connections | bot-connections:read, bot-connections:create, bot-connections:delete |
| StreamElements | se-tokens:read, se-tokens:create, se-tokens:delete |
| Extension Developer | extension-dev:read, extension-dev:create, extension-dev:edit, extension-dev:delete, extension-dev:publish, extension-dev:analytics, extension-dev:payouts |
| Extension Store | extension-store:read, extension-store:install, extension-store:uninstall, extension-store:configure, extension-store:review |
| Extension Data | extension-data:read, extension-data:edit |
| Widgets | widgets:read, widgets:create, widgets:edit, widgets:delete, widgets:access-read, widgets:access-grant, widgets:access-revoke |
| Sounds | sounds:read, sounds:create, sounds:edit, sounds:delete, sounds:play |
| Stream History | history:read, history:share, history:export, history:delete |
Adding an account permission means adding it to get_all_account_permission_infos() and seeding it for the owner role in a migration; the registry and both pickers then follow automatically.
User-Scope Permissions
10 permissions in rbac::user, granted through global user roles rather than account roles. They cover the Ideas Hub: ideas:read, ideas:create, ideas:edit, ideas:delete, ideas:vote, ideas:comment_read, ideas:comment_create, ideas:comment_edit, ideas:comment_delete, ideas:comment_vote. See Ideas Hub › Permissions.
Plan-Based Limits
Plan limits live in the canonical plans table (seeded rows; apps/api/src/db/plans.rs), not a hard-coded Rust table (ZAF-1092). These are defaults; the numeric limits below can be overridden per account via the account_limits table, except max_commands, which reads the plan row directly (no per-account override). 0 = unlimited.
| Limit | Free | Pro |
|---|---|---|
| Max overlays | 1 | 5 |
| Max storage | 100 MB | 1 GB |
| Max upload size | 10 MB | 50 MB |
| Max integrations | 2 | 10 |
| Chat retention | 7 days | 30 days |
| Max bot commands | 25 | 200 |
An Enterprise tier once existed, but its plan row was removed (migration 20260415000009_drop_enterprise_plan) and its accounts were moved to Pro; nothing resolves to it at runtime.
Capability toggles such as custom bot, OBS remote, and copyright detection are feature flags plus plan_features rows, not plan-limit columns — the boolean columns that once held them were dropped from account_limits when feature flags took over. See Billing.
Database
| Table | Database | Description |
|---|---|---|
account_roles | PostgreSQL | id, account_id, name, slug, description, is_default, is_system, color, sort_order, created_at, updated_at. UNIQUE(account_id, name) and UNIQUE(account_id, slug). |
account_role_permissions | PostgreSQL | (role_id, permission) join table — one row per granted permission, ON DELETE CASCADE |
account_memberships | PostgreSQL | Links users to accounts with a nullable role_id FK to account_roles |
admin_roles / admin_role_permissions | PostgreSQL | Admin-scope roles and their grants |
user_roles / user_role_permissions | PostgreSQL | User-scope roles and their grants |
Data Flow
- On account creation, the default roles (Owner, Administrator, Moderator, Viewer) are inserted into
account_roleswith their predefined permission sets written toaccount_role_permissions. - The account owner is assigned the Owner role automatically.
- When a user makes an API request, the auth middleware resolves their permissions:
- Check Redis cache for the user/account pair.
- If not cached, query the user's role and its permissions from PostgreSQL.
- Cache the result in Redis.
- GraphQL resolvers check permissions via
PermissionGuard::new("resource:action"). - REST endpoints check permissions via
auth.require_permission("resource:action"). - When a member's role is changed or a member is removed, the Redis cache is invalidated for that user/account pair.
Frontend gating mirrors the backend: <Gate permission="resource:action"> hides controls in both the dashboard and the admin app, but it is a UX layer — the backend guard is the authority.
Key Files
| Path | Description |
|---|---|
crates/lo-auth/src/rbac.rs | Permission registry (global / account / user), default roles |
crates/lo-graphql/src/guard.rs | PermissionGuard, AdminPermissionGuard, AuthGuard, SystemGuard |
crates/lo-api/src/extractors.rs | Auth extractor with require_permission() |
crates/lo-api/src/middleware/auth.rs | Permission cache and invalidate_permission_cache() |
crates/lo-websocket/src/gate.rs | Permission gates for WebSocket channels |
apps/api/src/graphql/roles.rs | GraphQL role queries and mutations |
apps/api/src/routes/roles.rs | REST role endpoints |