Skip to main content

Roles & Permissions

Overview​

The Roles & Permissions module implements a granular Role-Based Access Control (RBAC) system. Every account starts with 3 default roles (Owner, Administrator, Moderator), and custom roles can be created. The registry defines 133 account-scope permissions across 31 categories, plus 105 admin-scope permissions across 38 categories and 10 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 events → events:read, chat → chat:read, spotify → spotify:read, automations → automations:read, history → history:read; ext-storage takes extension-store:read in the account that owns the install; ext-install-logs takes bot-modules:read or automations:read according to the install's extension category.

Default Roles​

Defined as ROLE_OWNER / ROLE_ADMIN / ROLE_MODERATOR 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.

In the admin app, Add Member requires an account-owned role and offers the account's roles through adminAccountRoles / GET /v1/admin/accounts/\{id\}/roles (accounts:read). The picker hides the Owner/system role and leaves the role unselected until the operator chooses one; adding the member requires accounts:edit.

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. System-key-only grants such as bots:internal are not in the account registry either, so they are rejected here as unknown permissions.

Permissions​

Admin-Scope Permissions​

105 permissions, exhaustively enumerated by all_admin_permissions() in rbac.rs (pinned by all_admin_permissions_returns_expected_count); 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. audit:ingest is a system-key-only grant in effect; the ingest endpoint requires a System auth context and does not accept a human admin role grant.

CategoryPermissions
adminadmin:access, admin:privacy-erase, admin:privacy-objection-create, admin:privacy-objection-delete, admin:privacy-erase-withdraw
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, support:verify
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:ingest
audit-logaudit-log:read
csp-reportscsp-reports:read
devtoolsdevtools:read
platformsplatforms:read, platforms:edit
providersproviders:read, providers:edit
feature-flagsfeature-flags:read, feature-flags:edit
se-tokensse-tokens:read, se-tokens: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
developer-limitsdeveloper-limits:read, developer-limits:edit
developer-verificationdeveloper-verification:read, developer-verification:edit
developer-fundsdeveloper-funds:release-deletion, developer-funds:settle
discord-guildsdiscord-guilds:read, discord-guilds:delete
extension-reviewextension-review:read, extension-review:edit
extension-purchasesextension-purchases:read
contract-declarationscontract-declarations:read, contract-declarations:edit
oauth-clientsoauth-clients:read, oauth-clients:create, oauth-clients:edit, oauth-clients:delete
system-keyssystem-keys:read, system-keys:create, system-keys:delete
account-apikeysaccount-apikeys:read, account-apikeys:edit, account-apikeys:delete, account-apikeys:rotate
system-connectionssystem-connections:read, system-connections:edit, system-connections:delete
admin-rolesadmin-roles:read, admin-roles:create, admin-roles:edit, admin-roles:delete
copyrightcopyright:read, copyright:edit, copyright:delete, copyright:moderate
obsobs:read, obs:edit
bot-modulesbot-modules:read, bot-modules:edit
user-rolesuser-roles:read, user-roles:create, user-roles:edit, user-roles:delete
ideasideas:moderate_read, ideas:moderate_status, ideas:moderate_edit, ideas:moderate_delete, ideas:moderate_comment
crawlercrawler:scope-read, crawler:scope-edit, crawler:watchlist-read, crawler:watchlist-create, crawler:watchlist-delete

Several action strings, including bot-commands:read, copyright:read, obs:read, and se-tokens:read, are declared in both the global and account modules. The same string can therefore exist at system and account scope; handlers use the appropriate guard for the resource.

Account Permissions​

133 permissions in 31 categories, sourced from account::get_all_account_permission_infos() (pinned by account_permission_infos_has_expected_count) - 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, chat:automod-read, chat:automod-allow, chat:automod-deny, chat:pin, chat-bots:read, chat-bots:edit
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
API Keysapikeys:read, apikeys:create, apikeys:edit, apikeys: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
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, history:revenue-read, history:revenue-edit, history:keywords-edit
Setting Presetspresets:share
Public Statspublic-stats:read, public-stats:edit
Audit Logaudit-log:read

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
Channel data retention (chat, events, chatter profiles, moderation)365 days1095 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 Manager 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) 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::new(grant) (System key and a named grant — never type-only)
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