Skip to main content

Permissions

Lumio uses role-based access control (RBAC) to manage what each team member can do within an account.

How It Works​

Each account has roles (e.g., Owner, Administrator, Moderator). Each role has a set of permissions. When a user performs an action, the system checks if their role grants the required permission.

Permission checks run on all four protocol surfaces with the same resource:action strings:

  • REST - require_permission() in route handlers
  • GraphQL - PermissionGuard on resolvers
  • WebSocket - channel-subscribe gate in crates/lo-websocket/src/gate.rs
  • Frontend - <Gate permission> and hasPerm() in client code

The WebSocket layer maps each channel type to one of several gate kinds - a resource:action permission on the account in the channel name, a user-scoped permission, plain account scoping, a bound token type, or public. Most account-scoped streams use a :read permission (chat:\{account_id\} → chat:read, events:\{account_id\} → events:read). See WebSocket → RBAC gate for the full mapping. A caller that fails the gate receives code: "UNAUTHORIZED" on subscribe.

Default Roles​

Three roles are seeded for every new account, in this sort order: Owner, Administrator, Moderator. Accounts created earlier may have a fourth role named viewer: unused roles were deleted, while roles with a member or a redeemable invite are ordinary custom roles with unchanged permissions.

Owner (owner)​

Full account access. System role (is_system: true, cannot be deleted). Holds every account permission - the list is loaded explicitly, not via a wildcard.

Administrator (administrator)​

Everything Owner holds except account:delete, plan:read, plan:edit, and extension-dev:payouts. Account dissolution and plan changes are additionally enforced through an owner_id check rather than a permission.

Moderator (moderator)​

Chat moderation, event monitoring, Spotify playback control, and read access across the rest of the account.

CategoryPermissions
Eventsevents:read, events:create, events:edit, events:userinfo
Overlaysoverlays:read
Spotifyspotify:read, spotify:playback, spotify:queue, spotify:playlist, spotify:device
Chatchat:read, chat:write, chat:userinfo, chat:delete, chat:ban, chat:timeout, chat:notes, chat:raid, chat:pin, chat:refresh_user, chat:poll, chat:prediction, chat:automod-read, chat:automod-allow, chat:automod-deny
Connectionsconnections:read
Uploadsuploads:read
Rewardsrewards:read
Automationsautomations:read, automations:execute
Members & Rolesmembers:read, roles:read
OBSobs:read
Copyrightcopyright:vote, copyright:report, copyright:recommend
Botbot-modules:read, bot-connections:read, bot-commands:read, chat-bots:read
Extensionsextension-store:read, extension-store:review, extension-data:read
Widgetswidgets:read
Soundssounds:read, sounds:play
Stream Historyhistory:read, history:export
Setting Presetspresets:share
Public Statspublic-stats:read

Custom roles can be created under Dashboard > Settings > Roles.

Permission Reference​

The account registry holds 133 permissions, pinned by account_permission_infos_has_expected_count in crates/lo-auth/src/rbac.rs. One is assignable and seeded on the default roles but the API does not re-check it - the dashboard <Gate permission> hides the UI, but the endpoints do not gate on them, so assigning or withholding them changes nothing on the API surface:

PermissionWhere it takes effect
events:userinfoDashboard only - <Gate permission> hides the UI; the API does not re-check it

sessions:read/sessions:delete, automations:history, and events:delete were removed from the account registry in ZAF-1094 - they gated nothing (session management is self-scoped, there is no automation-history surface, and only the system retention sweep deletes events). copyright:report/copyright:recommend are not unenforced: they carry a PermissionGuard on the GraphQL mutations createCopyrightReport / createSafeRecommendation. There is simply no dedicated REST route - reporting and recommending both go through POST /v1/copyright/vote under copyright:vote.

The account role editor (GET /v1/roles/permissions and the GraphQL accountPermissions query) exposes exactly the account permissions listed below - the picker and the create/update validation registry share one source of truth (get_all_account_permission_infos()), so every permission you can assign is visible and vice-versa (ZAF-231). The admin panel's per-user and per-account permission-override pickers draw from the same source via the adminAssignablePermissions query (proxied as GET /api/assignable-permissions), so they list this exact set too and cannot drift (ZAF-430).

Events​

PermissionDescription
events:readView events, notifications, and event history
events:createSend test events and manage notifications
events:editEdit stored events: hide the buyer name of a Shopify order (hideShopifyOrderName, POST /v1/connections/shopify/events/\{event_id\}/hide-name). Owner and Administrator
events:userinfoView user cards and profiles from event entries

Overlays​

PermissionDescription
overlays:readView overlay configurations
overlays:createCreate new overlays
overlays:editEdit existing overlays
overlays:deleteDelete overlays
overlays:access-readRead per-overlay access entries/candidates and see all overlays (access-manager visibility)
overlays:access-grantGrant (set) per-overlay access for a user
overlays:access-revokeRevoke per-overlay access for a user

Widgets​

PermissionDescription
widgets:readView widget instances
widgets:createCreate and duplicate widget instances
widgets:editEdit widget configuration
widgets:deleteDelete widget instances
widgets:access-readRead per-widget access entries/candidates and bypass per-widget restrictions (access-manager visibility)
widgets:access-grantGrant (set) per-widget access for a user
widgets:access-revokeRevoke per-widget access for a user

Extension Data​

Gates direct account-actor calls to an installed extension's server functions (dashboard editor, lumio run CLI). Widget runtime calls authenticated by an Extension Token are governed by the install's own scope and are not gated by these permissions.

PermissionDescription
extension-data:readRead installed-extension data via query functions (queryRows, query-category handlers)
extension-data:editMutate installed-extension data via insertRow / patchRow / deleteRow and mutation/action handlers

Spotify​

PermissionDescription
spotify:readView now playing state, queue, devices, playlists, and search tracks
spotify:playbackControl playback: play, pause, skip, previous, seek, shuffle, repeat
spotify:volumeChange playback volume (gates the volume slider in the dashboard and popout player)
spotify:queueAdd tracks to the playback queue
spotify:playlistManage playlists: create, rename, delete, add/remove tracks, start playlist playback
spotify:deviceTransfer playback between Spotify devices
spotify:workerStart and stop the manual Spotify worker connect

Chat​

PermissionDescription
chat:readView chat messages, history, and emotes
chat:writeSend chat messages
chat:userinfoView user profiles, follow status, and moderation log
chat:deleteDelete chat messages
chat:banBan and unban users
chat:timeoutTimeout users
chat:notesView and manage user notes
chat:raidCancel active raids
chat:pinPin, re-time and unpin Twitch chat messages
chat:pollEnd active polls
chat:predictionEnd active predictions
chat:refresh_userManually refresh a user's platform profile data
chat:automod-readView Twitch messages held by AutoMod in the live chat and, with history:read, in a session's AutoMod log
chat:automod-allowAllow a held Twitch message with the acting member's own Twitch sign-in
chat:automod-denyDeny a held Twitch message with the acting member's own Twitch sign-in

Connections​

PermissionDescription
connections:readView connected platforms, credentials and live channel status (channelStatus, GET /v1/channel-status)
connections:createConnect new platforms
connections:editChange connection settings: the Shopify customer label and order details (updateShopifySettings, PUT /v1/connections/shopify/settings)
connections:deleteDisconnect platforms

Settings​

PermissionDescription
settings:readView account settings
settings:editModify account settings

Automations​

PermissionDescription
automations:readView automations and their configuration, and the handler logs of automation-node extension installs
automations:createCreate new automations
automations:editEdit automation metadata, nodes, edges
automations:deleteDelete automations
automations:executeManually trigger and start/stop automations

Members​

PermissionDescription
members:readView the members list and all invitations
members:createCreate invites, search users for direct invites
members:editChange member roles, create/edit/delete custom roles
members:deleteRemove members from the account, revoke pending invites

Uploads​

PermissionDescription
uploads:readView uploaded files
uploads:createUpload new files
uploads:deleteDelete uploaded files

Rewards​

PermissionDescription
rewards:readView channel point rewards
rewards:createCreate new rewards
rewards:editEdit existing rewards
rewards:deleteDelete rewards

Tokens​

PermissionDescription
tokens:readView popout tokens
tokens:createCreate popout tokens
tokens:editEdit popout tokens
tokens:deleteRevoke popout tokens

API Keys​

Personal user API keys (lm_usr_…) and account service keys (lm_svc_…) share one dedicated permission family, separate from popout tokens.

PermissionDescription
apikeys:readView API keys (personal lm_usr_ and account service lm_svc_)
apikeys:createCreate an API key (personal or account service key)
apikeys:editRename an API key (label only) and rotate its secret
apikeys:deleteRevoke an API key

A key owner can rotate their own key's secret under apikeys:edit - GraphQL rotateUserApiKey / rotateAccountServiceKey, REST POST /v1/api-keys/{id}/rotate / POST /v1/service-keys/{id}/rotate. Rotation regenerates the secret in place (id, label and scopes unchanged) and returns the new full key exactly once. Creating or rotating an account service key requires an interactive sign-in session and the caller must hold every permission carried by the key, including on rotation. API keys cannot mint or rotate service keys. (Scopes themselves stay immutable on the owner surface - see below.) Platform operators can additionally manage any account's/user's keys, including editing scopes, under the distinct admin-scope account-apikeys:* family.

tokens:create, tokens:edit and apikeys:create only let a caller mint or edit a token/key within their own permissions: it can never be granted a permission the caller does not currently hold. Requesting one is rejected with FORBIDDEN, identically on GraphQL and REST. A key or popout token that mints or edits a credential grants from its own capped scope (see API Keys), so a scope that went dormant after a demotion cannot be copied onto a new credential. A resource:* grant (system keys only) lets the caller grant any action under that resource; a bare *:* / * is not a global wildcard (it grants nothing beyond a literal *:* / *).

API-key scope registry (ZAF-1169). Unlike popout tokens - which keep a deliberately narrow, fixed list - an API key may carry any permission the account/user behind it holds, minus a small deny-list. The assignable-scope registry is derived from the RBAC registries and exposed on both protocols (GraphQL apiKeyAssignablePermissions(kind) / REST GET /v1/api-keys/permissions?kind=personal|service|developer), guarded on apikeys:read + feature:apikeys:

  • Personal keys (lm_usr_) can be assigned the account permissions and the user-scoped ideas:* family below - a personal key carries its member's user identity, so it can exercise user-scoped permissions (this is enforced end-to-end: require_user_permission accepts a personal API key that holds the scope).
  • Service keys (lm_svc_) can be assigned the account permissions only - an account-owned key has no user identity, so user-scoped permissions are meaningless on it and are never offered or honoured.
  • Developer keys (lm_dev_, ZAF-1176) can be assigned the extension-dev:* family only (derived from the account registry by prefix) - never an ordinary account scope such as chat:ban, never a user-scoped scope such as ideas:*. A developer key is a narrow publishing credential bound to a developer profile or team; it is minted from /v1/developer/api-keys (not the /v1/api-keys surface) and is not gated on feature:apikeys.
  • Deny-list (all kinds), never assignable: apikeys:create, apikeys:edit, tokens:create (credential minting - a key that mints credentials would outlive the human it belongs to) and account:delete (tenant destruction).

Requested scopes are validated against this registry on create - an unknown or deny-listed string is rejected with Invalid permission: {perm} (the same message role writes use), before the subset check. Scopes are immutable after creation; a too-narrow key must be re-minted.

Account​

PermissionDescription
account:readView account settings and information
account:editEdit account settings: the channel name/picture source, own name and own picture (/v1/accounts/{id}/identity-source, /name, /avatar), etc.
account:deleteDissolve (permanently delete) the account

Plan​

PermissionDescription
plan:readView account plan and billing information
plan:editChange account plan, manage subscription

Login Assignments​

Assignable to account roles, but enforced at the user level: own assignments are always allowed for any authenticated user, and these permissions only gate actions taken on behalf of other users on the account.

PermissionDescription
login-assignments:readView login assignments for the active account. Own assignments are always visible without this permission.
login-assignments:createAssign a login connection to an account on behalf of another user. Own assignments are always allowed without this permission.
login-assignments:deleteRemove a login assignment for the active account on behalf of another user. Own assignments are always removable without this permission.

Sessions​

Session management is not an account-role permission. Every authenticated user can view and revoke their own active browser sessions regardless of account role (and with no active account selected). The surface is own-scoped on both GraphQL (sessions / deleteSession / deleteAllOtherSessions) and REST (GET / DELETE /v1/users/me/sessions): the acting user's own id selects the rows, and the only gate is the first-party principal check (ZAF-469 / ZAF-516). The former sessions:read / sessions:delete role permissions were removed in ZAF-1094 - they gated nothing.

Extension Developer​

Extension developer permissions gate access to the developer dashboard and extension management. They are assignable to account roles and are also exercised through developer team RBAC. See Team-Scoped Permissions for the team permission model. This is also the entire scope surface of a developer API key (lm_dev_): such a key can carry any subset of this family and nothing else (ZAF-1176).

PermissionDescription
extension-dev:readView extension developer dashboard and owned extensions
extension-dev:createCreate new extensions
extension-dev:editEdit owned extensions (metadata, code, assets)
extension-dev:deleteDelete owned extensions
extension-dev:publishSubmit extensions for review and publish approved versions
extension-dev:analyticsView extension analytics (installs, usage, ratings)
extension-dev:payoutsManage extension revenue payouts

Sounds​

PermissionDescription
sounds:readView sounds
sounds:createUpload sounds
sounds:editEdit sound metadata
sounds:deleteDelete sounds
sounds:playPlay sounds in browser sources

Bot Modules​

PermissionDescription
bot-modules:readView bot module configurations, and the handler logs of bot-module extension installs
bot-modules:editEdit bot module configurations
PermissionDescription
copyright:readView safe/blocked songs, copyright check results
copyright:editAdd to this account's safe/blocked lists, import playlists
copyright:deleteRemove entries from safe/blocked lists
copyright:voteVote on community copyright-status candidates
copyright:reportReport a song's copyright status - GraphQL createCopyrightReport (PermissionGuard); no dedicated REST route (reporting goes through POST /v1/copyright/vote under copyright:vote)
copyright:recommendRecommend songs for the global list - GraphQL createSafeRecommendation (PermissionGuard); no dedicated REST route (recommending goes through POST /v1/copyright/vote under copyright:vote)

copyright:moderate is not an account permission - it is admin-scope (platform operator). See Admin: Copyright Moderation. It was moved out of account scope in ZAF-742 because approving/dismissing vote candidates writes the account-less global catalogue.

OBS​

PermissionDescription
obs:readView OBS integration configuration and status
obs:editEdit OBS integration settings and remote control
obs:deleteDelete the account's OBS integration configuration

Roles​

PermissionDescription
roles:readView account roles and the assignable-permission catalog
roles:editCreate and edit custom account roles
roles:deleteDelete custom account roles (system roles are protected server-side)

Role create/update validates every permission string against the account registry. A wildcard (resource:* / *:*) is rejected on both REST and GraphQL and can never be persisted into an account role.

Bot Commands​

PermissionDescription
bot-commands:readView chat commands and their per-platform overrides
bot-commands:createCreate chat commands
bot-commands:editEdit chat commands and set overrides
bot-commands:deleteDelete chat commands and clear overrides

Account Chat Bots​

PermissionDescription
chat-bots:readRead the account bot list and automatically recognised bots (Owner, Administrator, Moderator)
chat-bots:editMark or unmark a platform account as a bot (Owner and Administrator)

These account-scoped permissions govern Channel settings → Bots and the GraphQL/REST account-bot endpoints. They are separate from bot-connections:*, which manages the channel's connected bot identity.

Bot Connections​

PermissionDescription
bot-connections:readView the account's custom bot identities
bot-connections:createStart and complete the custom bot OAuth flow
bot-connections:deleteRemove a custom bot identity

Declared in both account and admin scope with the same string; the admin-scope copy gates system-level bot connections through require_admin_permission.

StreamElements​

PermissionDescription
se-tokens:readView stored StreamElements JWT tokens
se-tokens:createStore a StreamElements JWT token
se-tokens:deleteDelete a stored StreamElements JWT token

Extension Store​

PermissionDescription
extension-store:readBrowse the extension store and view installed extensions
extension-store:installInstall an extension into the account
extension-store:uninstallUninstall an extension from the account
extension-store:configureChange an installed extension's configuration
extension-store:reviewWrite store reviews and ratings

Stream History​

PermissionDescription
history:readView stream/session history, reports and stats; also unlocks the current paid subscriber count on channelConnections.paidSubscriberCount / GET /v1/connections/channel?include=paid_subscriber_count (null without it)
history:shareCreate, extend and revoke shareable report links
history:exportExport filtered history lists and finalized reports (CSV/PDF/TXT/JSON)
history:deleteDelete stored history sessions
history:revenue-readView estimated revenue, its converted amounts and assumptions; allow revenue in share links
history:revenue-editEdit revenue assumptions
history:keywords-editAdd, change or remove tracked chat keywords; history:read permits listing and pattern tests

Default roles: Owner and Administrator hold all seven; Moderator holds history:read + history:export. Gated behind the feature:stream_history feature flag.

Setting Presets​

PermissionDescription
presets:shareShare one of your own setting presets with the members of your active account

Default roles: Owner, Administrator and Moderator hold it. Only the act of sharing is gated - creating/editing/loading presets is user-scoped and needs no permission, and reading the shared section needs only account membership. Gated behind the feature:setting_presets feature flag.

Public Stats​

PermissionDescription
public-stats:readView this account's channel publicness settings for the public Stats app
public-stats:editToggle a channel's publicness / chatter-leaderboard suppression (opt-out / opt-in)

Default roles: Owner and Administrator hold both; Moderator holds public-stats:read. Gated behind the feature:public_stats_page feature flag.

Crawler control moved to admin scope (ZAF-725). The five crawler:* permissions that steer the platform-wide stats crawler are admin-scope (platform-operator) permissions, not account permissions - see Admin: Crawler below.

Audit Log​

PermissionDescription
audit-log:readRead this account's audit log (GET /v1/account/audit-log / accountAuditLog) - role/permission changes, member-role assignment, channel connections

Default roles: Owner and Administrator hold it; Moderator does not. This account-scope audit-log:read is a distinct permission from the identically-named admin-scope audit-log:read (see Admin: Support, Audit & StreamElements): the account grant reads a single account's log through require_permission, the admin grant reads the operator audit surface through require_admin_permission, and holding one never unlocks the other.

Admin-Scope Permissions​

Admin-scope permissions are checked against a user's admin role (not their account role). They gate the Lumio admin panel - /admin. The dashboard entry gate is admin:access, which is automatically included in every admin role.

Note: Some permissions (marked below) share an identical string with an account-scope permission but are enforced in a different context (require_admin_permission vs require_permission). This mirrors the bot-connections:* precedent.

Admin: Core Access​

PermissionDescription
admin:accessDashboard entry gate. Auto-injected into every admin role. Not a tenant bypass: it grants no access to another account's account-scoped surfaces (WebSocket channels, login assignments, OAuth callbacks).

Admin: Role Management​

PermissionDescription
admin-roles:readView admin roles, their permissions, and member lists
admin-roles:createCreate new admin roles
admin-roles:editEdit role name, description, permissions, and user assignments
admin-roles:deleteDelete admin roles (is_system roles are protected server-side)

Admin: Privacy​

PermissionDescription
admin:privacy-eraseErase persisted personal data on one channel's instruction (GDPR Art. 17). Powers DELETE /v1/admin/privacy/youtube/member/{id} (clears Lumio's cached YouTube member records across all accounts, and erases the member's chat data in the instructing account only) and POST /v1/admin/privacy/chat/erase (erases a data subject's chat messages, profile, moderator notes, AutoMod and moderation-log rows, bot-list entry and stream-history entries in the instructing account only, by Lumio user id or platform+user_id; both require account_id and instruction_reference, the chat erasure also reason), POST /v1/admin/privacy/lumio/erase (erases a person on Lumio's own surfaces only: the Lumio Stats suppression entry and, on YouTube, Lumio's member cache; no channel data; requires instruction_reference and reason), POST /v1/admin/privacy/records/erase (erases one record in the instructing account: a chat message, moderator note, moderation-log entry, AutoMod-log entry, bot-list entry or one person in one stream report; requires account_id, instruction_reference and reason), and POST /v1/admin/storage/orphaned-extension-uploads/sweep (finds - and with apply queues for deletion - extension upload files no row references any more). The chat, Lumio-only and record erasures are also available in the admin app under Moderation → Privacy Erasure (/privacy-erasure). Reserved for support tooling; assigned only to system_admin.
admin:privacy-objection-createRecord a chatter's GDPR Art. 21 objection in the global chatter_suppressions: the public Stats leaderboard masks the chatter on read, also on streams published earlier; channel reports, share links and AutoMod logs are not affected, and nothing is erased. Powers POST /v1/admin/privacy/chat/objections / GraphQL addChatterObjection. Assigned only to system_admin.
admin:privacy-objection-deleteLift a recorded chatter objection (never an erasure entry). Powers DELETE /v1/admin/privacy/chat/objections/{platform}/{user_id} / GraphQL removeChatterObjection. Assigned only to system_admin.
admin:privacy-erase-withdrawWithdraw one channel's erasure instruction for one chatter: deletes the chatter's row in that account's account_chatter_suppressions, so the channel's later reports name them again; nothing the erasure deleted comes back. Powers POST /v1/admin/privacy/chat/erase/withdraw / GraphQL withdrawChannelChatterSuppression. Assigned only to system_admin.

Admin: Developer Verifications​

PermissionDescription
developer-verification:readView developer applications and their details
developer-verification:editApprove or reject developer applications

Admin: Feature Flags​

PermissionDescription
features:readView feature flags
features:editToggle feature flags
feature-flags:readView the feature-flag registry detail view
feature-flags:editEdit feature-flag metadata (category, description, plan defaults)

Admin: Users & Accounts​

PermissionDescription
users:readView users, login connections, and membership details
users:editEdit user display name, email, overrides, and manage login connections
users:deleteDelete users
accounts:readView account details, limits, features
accounts:editUpdate account name, plan, limits, feature overrides, members, and reconnect flags
accounts:deleteDelete accounts
accounts:overrides-readRead per-account permission overrides
accounts:overrides-editSet or remove per-account permission overrides

Admin: Providers & Platforms​

PermissionDescription
providers:readView platform provider configurations
providers:editToggle providers and sub-flags
platforms:readView platform connection configuration
platforms:editEdit platform connection configuration

Admin: Infrastructure​

PermissionDescription
system-keys:readView system API keys; also reads the assignable-scope registry (adminSystemKeyPermissions / GET /v1/admin/system-key-permissions, ZAF-1213)
system-keys:createCreate system API keys (scopes validated against the derived assignable-scope registry, ZAF-1213)
system-keys:deleteDelete system API keys
oauth-clients:readView OAuth client registrations
oauth-clients:createCreate OAuth clients
oauth-clients:editEdit OAuth clients
oauth-clients:deleteDelete OAuth clients
system-connections:readView system-level platform connections
system-connections:editCreate and update system-level platform connections
system-connections:deleteDelete system-level platform connections

Admin: Account & User API Keys​

Platform-operator management of any account's or user's API keys - personal lm_usr_ keys (in api_keys) and account service lm_svc_ keys (in account_service_keys) - for support and incident response. Admin-scope (checked via require_admin_permission / AdminPermissionGuard), seeded to system_admin. This is a distinct family from the account-scope apikeys:*, which manages the caller's own keys; holding one never grants the other.

PermissionDescription
account-apikeys:readView any account's / user's API keys (never the secret)
account-apikeys:editEdit an API key's label and scopes
account-apikeys:rotateRegenerate an API key's secret (new plaintext returned once)
account-apikeys:deleteRevoke (delete) any API key

Scope edits validate against the ZAF-1170 assignable-scope registry for the key's kind (rejecting unknown, deny-listed, or user-scoped-on-service scopes). For a personal key the scopes are additionally capped to the key owner's own grantable permissions; for a service key the account is the principal, so the registry minus the deny-list is the ceiling - an operator can never push a key above its own principal. The rotated plaintext secret is returned exactly once, never in a list, never logged. Every operator action emits a system-scope audit event (system:api_key_updated / system:api_key_rotated / system:api_key_revoked) with the operator as the actor; these rows never surface in the target account's or user's own log. See Admin: API Keys.

Admin: Developer Funds​

PermissionDescription
developer-funds:release-deletionRelease a developer balance Lumio cannot pay out (no payout method, identity not verified) as a liability and delete the profile or the user, on the person's request. Always combined with the delete permission (users:delete or developer-verification:edit) and an interactive session.
developer-funds:settleRecord that a person presented the claim code of a released developer balance, and credit that balance to their new, verified developer profile. Interactive session required.

Admin scope, seeded to system_admin by 20261025200000_developer_released_balances (release-deletion) and 20261025210000_developer_funds_settle_permission (settle); never an account-role permission. GraphQL adminReleaseFundsAndDeleteUser / adminReleaseFundsAndDeleteDeveloper use AdminPermissionGuard plus the shared services::developer_funds_release::require_operator; the REST twins call the same helper. GraphQL adminStartReleasedBalanceClaim / adminSettleReleasedBalance use AdminPermissionGuard(DEVELOPER_FUNDS_SETTLE) plus services::developer_funds_settlement::require_operator; the REST twins POST /v1/admin/released-balances/{claim,settle} call the same helper.

Admin: Contract Declarations​

The withdrawal and cancellation queue spans all accounts. These are admin-scoped grants, seeded to system_admin; they are not account-role permissions.

PermissionDescription
contract-declarations:readList and inspect declarations across accounts.
contract-declarations:editMatch an open public case, close a case, or extend retention; each mutation also requires an interactive session.

The admin page is Contract Declarations. REST uses require_admin_permission; GraphQL uses AdminPermissionGuard, with matching permission strings.

Admin: Billing, Plans & Coupons​

PermissionDescription
billing:readView billing data
billing:editModify billing data
plans:readView plans
plans:createCreate plans
plans:editEdit plans and their feature mappings
plans:deleteDelete plans
subscriptions:readView subscriptions
subscriptions:editModify subscriptions
coupons:readView coupons
coupons:createCreate coupons
coupons:editEdit coupons
coupons:deleteDelete coupons

Admin: Bot Control & Commands​

PermissionDescription
bot-control:readView bot enabled/disabled status across all accounts
bot-control:editToggle the bot on or off for an account
bot-commands:readView chat commands across all accounts
bot-commands:createCreate chat commands on any account
bot-commands:editEdit chat commands on any account
bot-commands:deleteDelete chat commands on any account
bot-connections:readView system bot connections
bot-connections:createCreate system bot connections
bot-connections:deleteDelete system bot connections
discord-guilds:readView Discord guild connections across all accounts
discord-guilds:deleteDelete a Discord guild connection

Admin: Support, Audit & StreamElements​

PermissionDescription
support:readView support tickets
support:editHandle support tickets
support:verifyReveal or check a user's hourly support code and read recent checks; admin scope only. GraphQL and REST use the same admin guard.
audit:readRead the audit log
audit:ingestServer-to-server user-scope audit ingest - the narrow grant apps/id needs to write user:login / user:oauth_granted to POST /v1/internal/audit-ingest. Declared in the admin registry so the system-key mint accepts it, but system-key-only in effect: authorize_ingest requires an AuthContext::System, so a human admin holding it in a role can never write. Never seeded into any admin role (ZAF-1212).
audit-log:readRead the admin (system-scope) audit log (GET /v1/admin/audit-log). Distinct from the identically-named account-scope audit-log:read, which reads a single account's log via require_permission; this admin copy is checked via require_admin_permission and holding one never unlocks the other.
csp-reports:readRead the CSP-violation calibration aggregate - cspViolationReports / REST GET /v1/admin/csp-reports. Distinct (violated_directive, blocked_host) pairs with counts, from the capped csp_violation_reports TimescaleDB table (≤5000 pairs, ≤30 d rolling). Admin-scope (platform operator) - seeded to system_admin, not assignable to any tenant role (ZAF-1051).
devtools:readOpen the Dev & Admin menu in the web app (lumio.vision, production included). A pure UI-visibility grant: the web app reads it from me { adminPermissions } / REST GET /v1/users/me (admin_permissions) - both resolve the same admin-role rows - and no resolver or route is guarded on it. The menu's actions (e.g. previewing a seasonal look) change only the viewer's own browser and are never sent to the server, so it emits no audit event. Admin-scope (platform operator) - seeded to system_admin, assignable to further admin roles via Admin Roles, not assignable to any tenant role (ZAF-1668).
se-tokens:readView StreamElements tokens across all accounts
se-tokens:deleteDelete a StreamElements token on any account

Admin: Extensions & Developers​

PermissionDescription
extension-review:readView the extension review queue and submission details
extension-review:editApprove, reject, or request changes on extension submissions
developer-limits:readView developer rate limits and quotas
developer-limits:editEdit developer rate limits and quotas
extension-purchases:readRead paid extension checkouts across all accounts that were never delivered - GraphQL adminUndeliveredExtensionPurchases / REST GET /v1/admin/extension-purchases/undelivered (Admin › Extension Purchases). Admin scope - seeded to system_admin, not assignable to any tenant role (ZAF-2745).

Admin: Ideas Moderation​

PermissionDescription
ideas:moderate_readView all ideas with moderation details (reports, flags, internal notes)
ideas:moderate_statusChange idea status (approve, reject, mark as planned, close)
ideas:moderate_editEdit any idea regardless of ownership
ideas:moderate_deleteDelete any idea regardless of ownership
ideas:moderate_commentDelete any comment on ideas regardless of ownership (comment editing is author-only)
PermissionDescription
copyright:moderateApprove/dismiss community vote candidates into the global cross-account safe/blocked catalogue (approveReport/dismissReport + REST /v1/copyright/vote-candidates/{track_id}/approve·/dismiss). Admin-scope (platform operator) - seeded to system_admin, not assignable to any tenant role (ZAF-742).

Admin: User Roles​

PermissionDescription
user-roles:readView user roles and their permissions
user-roles:createCreate new user roles
user-roles:editEdit user role name, description, permissions, and assignments
user-roles:deleteDelete user roles (is_system roles are protected server-side)

Admin: Crawler​

Platform-operator control of the public stats crawler - a single global resource (one crawler, one global scope flip). Moved from account scope to admin scope in ZAF-725; enforced via require_admin_permission / AdminPermissionGuard and seeded to the system_admin role only.

PermissionDescription
crawler:scope-readRead the public stats crawler's scope mode (GET /v1/crawler/scope / crawlerScope)
crawler:scope-editSet the crawl scope mode, incl. the opt-in global flip (PUT /v1/crawler/scope / setCrawlerScope)
crawler:watchlist-readList the crawl watchlist (GET /v1/crawler/watchlist / crawlerWatchlist)
crawler:watchlist-createAdd a channel to the crawl watchlist (POST /v1/crawler/watchlist / addCrawlerWatchlistChannel)
crawler:watchlist-deleteRemove a channel from the crawl watchlist (DELETE /v1/crawler/watchlist/{platform}/{platform_channel_id} / removeCrawlerWatchlistChannel)

Actions are granular (the coarse :manage is disallowed); each scope change or watchlist mutation emits a system-scope audit event. The Admin app additionally gates entry on admin:access.

Admin: Shared with Account Scope​

Seventeen permission strings are declared in both admin scope and account scope. They share the same string value but are enforced by different guards (require_admin_permission against the user's admin role vs require_permission against their account role), so holding one does not grant the other:

PermissionsCategory
copyright:read, copyright:edit, copyright:deleteCopyright
obs:read, obs:editOBS
bot-modules:read, bot-modules:editBot Modules
bot-commands:read, bot-commands:create, bot-commands:edit, bot-commands:deleteBot Commands
bot-connections:read, bot-connections:create, bot-connections:deleteBot Connections
se-tokens:read, se-tokens:deleteStreamElements
audit-log:readAudit Log

System-Key-Only Permissions​

Grants that exist only for system keys. They are listed in lo_auth::rbac::system_key::SYSTEM_ONLY_PERMISSIONS, offered by the system-key mint registry (adminSystemKeyPermissions / GET /v1/admin/system-key-permissions), and absent from the account, user and admin (operator) registries - so no role can ever hold them: create_role / update_role reject them as unknown permissions on both protocols. Each guarded site checks them with AuthContext::require_system_permission, which requires a System key and the grant.

PermissionDescription
bots:internalThe bot fleet's internal surface (ZAF-1922): GraphQL botChannels, botCommandsForAccount, emitBotEvent, discordBotGuilds, discordBotGuildFeatures, discordBotAllGuildFeatures, updateDiscordGuildInfo, removeDiscordGuildByBot, the bot chat ingest (sendChatMessage / POST /v1/chat/message) and the REST feeds GET /v1/internal/bot-modules/\{account_id\}/triggers, GET /v1/internal/bot-modules/handlers, GET /v1/internal/automation-nodes/handlers; and the cross-account extension and live surfaces (ZAF-1976): extension server functions on any install (extensionQuery, extensionMutate, extensionExecuteHandler; REST POST /v1/extension-installs/\{install_id\}/\{query,mutate,execute\}/\{function_name\}), draft/testing extension file trees (extensionVersionFiles; REST GET /v1/extensions/\{id\}/versions/\{version\}/files) and bundle bytes (GET /v1/extension-bundles/...), and the WebSocket presence:* and sounds:* channels. A non-System caller gets FORBIDDEN (System access required; REST feeds: System key required for internal endpoints); a System key without the grant gets Missing permission: bots:internal. Carried by the keys of twitch-bot, kick-bot, youtube-bot, discord-bot, bot-module-worker and automation-worker.

audit:ingest (see Admin: Support, Audit & StreamElements) is system-key-only in effect by a different route: it is declared in the admin registry, but its only consumer requires a System key.

User-Scoped Permissions​

User-scoped permissions are global - they are not tied to a specific account. They control access to platform-wide features that exist outside the account context, such as the Ideas Hub. User-scoped permissions are assigned via user roles (user_roles / user_role_permissions), not account roles. Enforcement uses auth.require_user_permission() on the backend, which is satisfied by a logged-in user session or a personal API key (lm_usr_) that carries the scope - a personal key acts with its member's user identity (ZAF-1169). An account service key (lm_svc_) has no user identity and is always rejected here, fail-closed.

Ideas Hub​

PermissionDescription
ideas:readRead ideas
ideas:createCreate new ideas
ideas:editEdit own ideas
ideas:deleteDelete own ideas
ideas:voteVote on ideas
ideas:comment_readRead comments on ideas
ideas:comment_createCreate comments on ideas
ideas:comment_editEdit own comments
ideas:comment_deleteDelete own comments
ideas:comment_voteVote on comments and replies

Default User Roles​

Three system user roles are seeded by default:

Member​

Assigned to all users automatically. Grants full Ideas Hub participation.

CategoryPermissions
Ideasideas:read, ideas:create, ideas:edit, ideas:delete, ideas:vote
Commentsideas:comment_read, ideas:comment_create, ideas:comment_edit, ideas:comment_delete, ideas:comment_vote

Restricted​

Limited to read-only access.

CategoryPermissions
Ideasideas:read
Commentsideas:comment_read

Moderator​

Full user permissions plus admin-level moderation capabilities. The five ideas:moderate_* permissions are admin-scoped - see Admin: Ideas Moderation above.

CategoryPermissions
Ideasideas:read, ideas:create, ideas:edit, ideas:delete, ideas:vote
Commentsideas:comment_read, ideas:comment_create, ideas:comment_edit, ideas:comment_delete, ideas:comment_vote
Moderation (admin-scoped)ideas:moderate_read, ideas:moderate_status, ideas:moderate_edit, ideas:moderate_delete, ideas:moderate_comment

Team-Scoped Permissions (Developer Teams)​

Team-scoped permissions control what each member can do within a developer team. They are separate from both account-scope and admin-scope permissions and are defined in crates/lo-auth/src/rbac.rs::team. Team management uses require_team_permission() in REST and an equivalent GraphQL helper. Extension actions use the same shared team-role check on both protocols after ownership is established. That check reads the role from the database, so a role change applies on the next extension request. Missing grants return 403 Missing team permission: <permission>; non-members fail the ownership check first.

Extension Development​

PermissionDescription
team-extensions:createCreate extensions within the team
team-extensions:editUpdate metadata; upload or delete icons and screenshots
team-extensions:deleteDelete a draft extension or request deletion
team-extensions:publishSubmit a version for review
team-extensions:analyticsView extension analytics
team-versions:createCreate or upload an extension version
team-versions:editEdit extension versions

Secrets​

PermissionDescription
team-secrets:readList the key names of a team extension's secrets (…/secrets, developerExtensionSecrets)
team-secrets:editSet or delete a team extension's secrets; create, rename and revoke the team's developer API keys

Testers​

PermissionDescription
team-testers:readList testers, private access grants, and access invites
team-testers:inviteInvite testers, grant private access, and create access invites
team-testers:removeRemove testers, revoke private access, and delete access invites

Team Members​

PermissionDescription
team-members:readRead team member list
team-members:inviteInvite new team members
team-members:editEdit team member roles
team-members:removeRemove team members

Team Settings​

PermissionDescription
team-settings:readRead team settings
team-settings:editEdit team settings

Payouts​

PermissionDescription
team-payouts:readRead team payout information
team-payouts:editEdit team payout configuration

team-versions:edit, team-settings:read, and team-payouts:read/team-payouts:edit are present in the team-role catalogue, but no current extension action checks those grants. Assigning one does not grant a version-edit or payout action by itself.

Default Team Roles​

Three roles are seeded for every new developer team:

Owner (team-owner)​

System role (cannot be deleted). All 20 team permissions.

Admin (team-admin)​

Manage extensions and team members. All permissions except team-settings:edit, team-payouts:edit, and team-members:remove.

Member (team-member)​

View and develop extensions. Permissions: team-extensions:create, team-extensions:edit, team-extensions:analytics, team-versions:create, team-members:read, team-secrets:read, team-settings:read.

Members can edit and create versions, but cannot submit, delete, manage private access or testers, or list testers without a custom role granting those actions. Solo extensions have no team-role gate. A developer API key bound to a team can act only on that team's extensions.

Popout Tokens​

Popout tokens inherit specific permissions when created. A popout token can only have permissions that the creating user's role grants. For example, a Moderator cannot create a popout token with settings:edit.

The /popout/test-events link needs events:create in its own permission list to show the catalog and send a test. The Events feed popout uses events:read. A token without events:create sees a permission notice on the test page and cannot send. spotifyManualStatus remains gated on spotify:read; live channelStatus uses connections:read on GraphQL and REST.

Overlay Tokens​

Overlay tokens (lm_overlay_*) have no RBAC permissions. They cannot access any REST or GraphQL endpoint. Their sole capability is subscribing to the overlay:{key} WebSocket channel where the token's bound overlay_id matches the resolved overlay key.

  • has_permission(_) always returns false
  • Cannot broadcast to any channel
  • Cannot subscribe to any channel other than the one matching their bound overlay
  • Rate-limited at 600 requests/min (same tier as Popout tokens)

Shared Overlay Tokens​

Shared overlay tokens (lm_share_*) provide temporary, time-limited access to a specific overlay. They are designed for sharing overlays with collaborators or backup streaming setups without granting permanent account access.

  • Prefix: lm_share_ (9 characters)
  • Body: 32 random bytes, hex-encoded (64 characters)
  • Total length: 73 characters
  • Transport: ?token=lm_share_… query parameter or Authorization: Bearer lm_share_… header
  • Rate-limited at 600 requests/min (same tier as Popout and Overlay tokens)
  • has_permission(_) always returns false
  • Cannot access REST or GraphQL endpoints
  • Can only subscribe to the overlay:{key} WebSocket channel matching the token's bound overlay
  • Server checks expiry every heartbeat tick (5s) - disconnects with TOKEN_EXPIRED when elapsed
  • Server checks DB revocation every 30 seconds - disconnects with TOKEN_REVOKED if revoked (the same credential re-validation every token-authenticated WebSocket session gets)

Token Types​

TypePrefixLengthPurpose
System Keylm_sys_71Internal service-to-service auth
User API Keylm_usr_71External API access
JWTlm_ + eyJ...variableSession-based auth
Popout Tokenlm_pop_71Overlay/popout access with a scoped permission subset
Overlay Tokenlm_overlay_75Per-overlay browser source access
Shared Overlay Tokenlm_share_73Temporary shared overlay link
Extension Tokenlm_ext_71Extension iframe, scoped to one install
Widget Tokenlm_widget_74Per-widget-instance browser source access

Popout tokens accept an optional expires_at; absent or null means the token never expires. See Authentication for transports and rate limits.

Extension Permissions​

Extension tokens (lm_ext_*) have no RBAC permissions (has_permission() always returns false). Instead, extensions declare required permissions in lumio.config.json and are granted only those capabilities at install time. The Lumio API validates each action call against the extension's declared permission set.

Available Extension Permissions​

The manifest validator accepts exactly these strings in lumio.config.json permissions - anything else fails validation at upload:

PermissionDescriptionReview level
events:readRead events from the Lumio event busStandard
events:emitEmit custom eventsStandard
chat:readRead chat messagesStandard
chat:sendSend chat messagesStandard
chat:deleteDelete chat messagesEnhanced
chat:banBan and unban users, timeout usersEnhanced
obs:set_sceneSwitch the active OBS sceneStandard
obs:set_source_visibleToggle OBS source visibilityStandard
obs:start_streamStart the OBS streamStandard
obs:stop_streamStop the OBS streamStandard
overlay:update_layerUpdate overlay layer propertiesStandard

Enhanced review: Extensions requesting chat:ban or chat:delete receive additional scrutiny during the approval process. Reviewers verify that the extension implements appropriate safeguards (e.g., confirmation prompts, configurable thresholds, exempt role support) and that the moderation logic does not create excessive false positives. The chat:ban permission grants ban, unban, and timeout capabilities through a single declaration.

Action-to-declaration mapping​

At call time, each action resolves to the manifest permission the install must have declared; an undeclared permission is rejected before the action runs:

ActionRequired declaration
chat:send, chat:deletechat:read
events:emitevents:read
overlay:update_layeroverlays:edit
obs:set_scene, obs:set_source_visible, obs:start_stream, obs:stop_streamobs:control

obs:control and overlays:edit are the strings this mapping checks for; they are not among the values the manifest validator accepts, so the OBS and overlay-layer actions cannot currently be authorised through a validated manifest.

API Keys​

A User API key carries an explicit permission list stored on the key row. At creation that list can never exceed what the creator holds. On every request the key is also capped to its owner's current role: the auth middleware keeps only the stored scopes that the owner's account-role and user-scoped permissions still grant. A member demoted from Administrator to Viewer therefore loses every Administrator scope on their keys at the next request, and a re-promotion restores them, because the stored list is never rewritten. Popout tokens and popout sessions are capped to their bound member the same way. The ceiling comes from the same permission cache a signed-in session uses, so a role change or role edit reaches keys exactly as fast as it reaches the dashboard. If the ceiling cannot be read, the request fails with 503 authorization_unavailable; it never falls back to the stored list. Account owners without a membership row are capped to the account's owner role. GET /v1/tokens/me (GraphQL myPermissions) reports the capped scope, never a wildcard. Key and popout-token responses carry both permissions (stored) and effective_permissions (what authorizes requests now); a stored scope missing from effective_permissions is dormant. Service keys (lm_svc_) and developer keys (lm_dev_) have no bound member and are not capped this way. Keys may carry an expires_at; an expired key resolves to Anonymous rather than erroring. Rate limits differ by auth type - see Authentication.