Skip to main content

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), and user (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::PermissionGuard for GraphQL resolvers, Auth::require_permission() for REST endpoints; both resolve against the active account. AdminPermissionGuard / require_admin_permission() and require_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 the account_role_permissions join 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_for in crates/lo-websocket/src/gate.rs maps eventsevents:read, chatchat:read, spotifyspotify:read, automationsautomations:read, historyhistory:read, ext-storageextension-store:read, ext-install-logsbot-modules:read.

Default Roles

Defined as ROLE_OWNER / ROLE_ADMIN / ROLE_MODERATOR / ROLE_VIEWER in rbac.rs.

RoleSlugColorSystemSortDescription
Ownerowner#f59e0bYes0Full account access — every account permission, listed explicitly. Cannot be deleted or assigned to another member.
Administratoradministrator#ef4444No1Full access except account deletion and plan changes (enforced via the owner_id check, not by permissions).
Moderatormoderator#22c55eNo2Chat moderation and event monitoring.
Viewerviewer#6b7280No3Read-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

QueryReturnsPermission
roles[Role!]!roles:read
role(id: UUID!)RoleDetailroles: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

MutationArgsReturnsPermission
createRoleinput: CreateRoleInput!RoleDetail!roles:edit
updateRoleinput: UpdateRoleInput!RoleDetail!roles:edit
deleteRoleid: 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.

MethodPathPermissionDescription
GET/v1/rolesroles:readList roles for the account
GET/v1/roles/{id}roles:readGet a single role with its permissions
POST/v1/rolesroles:editCreate a role (name, description, color, permissions)
PATCH/v1/roles/{id}roles:editUpdate role name, description, color, or permissions
DELETE/v1/roles/{id}roles:deleteDelete a role (system roles cannot be deleted)
GET/v1/roles/permissionsroles:readCatalog 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.

CategoryPermissions
adminadmin:access, admin:privacy-erase
usersusers:read, users:edit, users:delete
accountsaccounts:read, accounts:edit, accounts:delete, accounts:overrides-read, accounts:overrides-edit
billingbilling:read, billing:edit
supportsupport:read, support:edit
featuresfeatures:read, features:edit
plansplans:read, plans:create, plans:edit, plans:delete
subscriptionssubscriptions:read, subscriptions:edit
couponscoupons:read, coupons:create, coupons:edit, coupons:delete
auditaudit:read
audit-logaudit-log:read
platformsplatforms:read, platforms:edit
providersproviders:read, providers:edit
feature-flagsfeature-flags:read, feature-flags:edit
oauth-clientsoauth-clients:read, oauth-clients:create, oauth-clients:edit, oauth-clients:delete
system-keyssystem-keys:read, system-keys:create, system-keys:delete
system-connectionssystem-connections:read, system-connections:edit, system-connections:delete
admin-rolesadmin-roles:read, admin-roles:create, admin-roles:edit, admin-roles:delete
user-rolesuser-roles:read, user-roles:create, user-roles:edit, user-roles:delete
bot-commandsbot-commands:read, bot-commands:create, bot-commands:edit, bot-commands:delete
bot-controlbot-control:read, bot-control:edit
bot-connectionsbot-connections:read, bot-connections:create, bot-connections:delete
bot-modulesbot-modules:read, bot-modules:edit
discord-guildsdiscord-guilds:read, discord-guilds:delete
se-tokensse-tokens:read, se-tokens:delete
copyrightcopyright:read, copyright:edit, copyright:delete
obsobs:read, obs:edit
extension-reviewextension-review:read, extension-review:edit
developer-verificationdeveloper-verification:read, developer-verification:edit
developer-limitsdeveloper-limits:read, developer-limits:edit
ideasideas: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.

CategoryPermissions
Eventsevents:read, events:create, events:userinfo
Overlaysoverlays:read, overlays:create, overlays:edit, overlays:delete, overlays:access-read, overlays:access-grant, overlays:access-revoke
Spotifyspotify:read, spotify:playback, spotify:volume, spotify:queue, spotify:playlist, spotify:device, spotify:worker
Chatchat:read, chat:write, chat:userinfo, chat:delete, chat:ban, chat:timeout, chat:notes, chat:raid, chat:refresh_user, chat:poll, chat:prediction
Connectionsconnections:read, connections:create, connections:edit, connections:delete
Settingssettings:read, settings:edit
Accountaccount:read, account:edit, account:delete
Planplan:read, plan:edit
Membersmembers:read, members:create, members:edit, members:delete
Rolesroles:read, roles:edit, roles:delete
Login Assignmentslogin-assignments:read, login-assignments:create, login-assignments:delete
Uploadsuploads:read, uploads:create, uploads:delete
Rewardsrewards:read, rewards:create, rewards:edit, rewards:delete
Tokenstokens:read, tokens:create, tokens:edit, tokens:delete
Automationsautomations:read, automations:create, automations:edit, automations:delete, automations:execute
OBSobs:read, obs:edit, obs:delete
Copyrightcopyright:read, copyright:edit, copyright:delete, copyright:vote, copyright:report, copyright:recommend, copyright:moderate
Bot Modulesbot-modules:read, bot-modules:edit
Bot Commandsbot-commands:read, bot-commands:create, bot-commands:edit, bot-commands:delete
Bot Connectionsbot-connections:read, bot-connections:create, bot-connections:delete
StreamElementsse-tokens:read, se-tokens:create, se-tokens:delete
Extension Developerextension-dev:read, extension-dev:create, extension-dev:edit, extension-dev:delete, extension-dev:publish, extension-dev:analytics, extension-dev:payouts
Extension Storeextension-store:read, extension-store:install, extension-store:uninstall, extension-store:configure, extension-store:review
Extension Dataextension-data:read, extension-data:edit
Widgetswidgets:read, widgets:create, widgets:edit, widgets:delete, widgets:access-read, widgets:access-grant, widgets:access-revoke
Soundssounds:read, sounds:create, sounds:edit, sounds:delete, sounds:play
Stream Historyhistory: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.

LimitFreePro
Max overlays15
Max storage100 MB1 GB
Max upload size10 MB50 MB
Max integrations210
Chat retention7 days30 days
Max bot commands25200

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

TableDatabaseDescription
account_rolesPostgreSQLid, 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_permissionsPostgreSQL(role_id, permission) join table — one row per granted permission, ON DELETE CASCADE
account_membershipsPostgreSQLLinks users to accounts with a nullable role_id FK to account_roles
admin_roles / admin_role_permissionsPostgreSQLAdmin-scope roles and their grants
user_roles / user_role_permissionsPostgreSQLUser-scope roles and their grants

Data Flow

  1. On account creation, the default roles (Owner, Administrator, Moderator, Viewer) are inserted into account_roles with their predefined permission sets written to account_role_permissions.
  2. The account owner is assigned the Owner role automatically.
  3. 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.
  4. GraphQL resolvers check permissions via PermissionGuard::new("resource:action").
  5. REST endpoints check permissions via auth.require_permission("resource:action").
  6. 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

PathDescription
crates/lo-auth/src/rbac.rsPermission registry (global / account / user), default roles
crates/lo-graphql/src/guard.rsPermissionGuard, AdminPermissionGuard, AuthGuard, SystemGuard
crates/lo-api/src/extractors.rsAuth extractor with require_permission()
crates/lo-api/src/middleware/auth.rsPermission cache and invalidate_permission_cache()
crates/lo-websocket/src/gate.rsPermission gates for WebSocket channels
apps/api/src/graphql/roles.rsGraphQL role queries and mutations
apps/api/src/routes/roles.rsREST role endpoints