System Keys
System Keys are long-lived API keys used for service-to-service authentication against the Lumio API. Unlike OAuth clients (which act on behalf of users) or user API keys (which act on behalf of a single account), a system key authenticates an internal Lumio service such as an external bot worker, a deployment health-check script, or a backend job. A system key is not bound to an account: it can act on any account, but only within the scopes it was minted with - a key reaches exactly what its scopes allow and nothing more. Mint them sparingly and with the narrowest scope set the consumer needs.
Where to find it
Admin sidebar → System (/system) - the System API Keys card. System Keys and System Connections share this page.
Quick start
- Open Admin → System.
- Scroll to the System API Keys card.
- Click Create Key.
- Enter a descriptive label (e.g.
Twitch Bot Key) and select at least one scope from the searchable scope picker. - Click Create Key and copy the revealed key immediately - it is shown exactly once, stored hashed in the DB.
- Use it as a bearer token in the
Authorizationheader for internal service calls.
Detailed walkthrough
System API Keys card
Shows a key count ("N key(s)") and one card per key, not a table. Each key card shows:
- Label - human-friendly name given at creation, with the key's non-secret prefix in monospace next to it.
- Permissions - the scoped permission list, as chips.
- Created and Last used - dates on one line; "Last used" is omitted when the key has never been used.
- Revoke - a red button, gated on
system-keys:delete. It deletes immediately with no confirmation prompt.
Create System API Key form
Clicking Create Key (gated on system-keys:create) opens an inline card form, not a dialog:
- Label - free text, e.g.
Twitch Bot Key. - Scopes - a searchable, category-grouped picker (ZAF-1214). The Create button stays disabled until a label is entered and at least one scope is selected - and while the scope registry is still loading or failed to load.
The picker is driven entirely by the backend registry - never a hardcoded list. On open it fetches GET /v1/admin/system-key-permissions (GraphQL adminSystemKeyPermissions, guarded on system-keys:read) and renders every returned scope, grouped by category with a search box across the ~130 scopes. Category headers are localised (en/de); a category with no translation falls back to a humanised form of its registry key, so a scope is never silently dropped. Each scope is shown by its canonical key (e.g. chat:write) - the identifier the mint validates - rather than a separately maintained label that could drift (ZAF-428). Because the picker consumes the same registry the mint validates against, it can never offer a scope the create path rejects. Before ZAF-1213/1214 the dialog rendered a hardcoded 13-string list that the mint validated against the operator registry alone - every one of those chips was rejected and the whole flow was non-functional.
The assignable registry is lo_auth::rbac::system_key::get_assignable_permissions() (ZAF-1213): it unions the platform-operator registry (all_admin_permissions()), the account-scope service registry (events:*, chat:*, overlays:*, connections:*, …) and the user-scope registry, then removes a two-category deny-list: credential minting (system-keys:create, apikeys:create/apikeys:edit, tokens:create, admin-roles:create/edit/delete, oauth-clients:create) and tenant/platform destruction (accounts:delete, account:delete, admin:privacy-erase). On top of that it adds the system-only grants (system_key::SYSTEM_ONLY_PERMISSIONS) - today bots:internal (category bots), which exists for system keys alone and is in no role registry (ZAF-1922). Both create paths (adminCreateSystemKey and POST /v1/admin/system-keys) validate every requested permission against this one registry.
Empty and error states are explicit, not a silently blank list. The registry read is guarded on system-keys:read, which is a different guard from the system-keys:create gate on the Create button - an operator holding only system-keys:create can open the form but get a 403 on the registry read. In that case the picker shows an error state naming the missing system-keys:read requirement and a Retry button, and Create stays disabled. A registry that resolves but is empty shows a distinct "nothing assignable" notice.
Wildcards (admin:*, *:*) are not mintable through this dialog (ZAF-1213). The self-service mint path rejects them. The API does not load config-provisioned System keys. An operator-class key that needs platform-admin reach is granted the explicit admin:access scope (or specific admin permissions), never a wildcard. There is no admin:access auto-injection on the system-key mint - a machine credential is not a dashboard seat.
audit:ingest is the narrow grant the identity app (apps/id) needs to write user-scope security events (user:login, user:oauth_granted) to POST /v1/internal/audit-ingest. It is system-key-only in effect: the ingest endpoint requires a System key, so a human admin who happens to hold audit:ingest in an admin role can never write with it. Mint a dedicated key that carries only audit:ingest and nothing else - see Provisioning the apps/id audit-ingest key.
bots:internal is the grant for the bot fleet's internal surface (ZAF-1922): the bot channel/command/event feeds the platform bots read (botChannels, botCommandsForAccount, emitBotEvent, the discordBot* resolvers, updateDiscordGuildInfo, removeDiscordGuildByBot), the internal bot-module and automation-node feeds (GET /v1/internal/bot-modules/\{account_id\}/triggers, GET /v1/internal/bot-modules/handlers, GET /v1/internal/automation-nodes/handlers) the bot chat ingest (POST /v1/chat/message / sendChatMessage), 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. Each of these requires a system key and this grant - a system key without it is refused with Missing permission: bots:internal. It is system-only: unlike audit:ingest (declared in the operator registry, system-key-only in effect), it is in no role registry at all - not account, user or operator - so no role can hold it. See Provisioning bot and worker keys.
Submit invokes POST /v1/admin/system-keys; the response contains the plaintext key only once.
API Key Created reveal dialog
Warning-styled, one-time display:
- Title: "API Key Created - Save Your Key".
- Body: "This key will only be shown once. Copy it now and store it securely."
- Buttons: Copy Key, Dismiss.
Revocation
Revoke deletes the key row immediately - there is no confirmation dialog. Any subsequent request using that key returns 401. There is no soft-disable / re-enable flow - rotation is delete + recreate.
Common scenarios
- Mint a key for a new bot worker - label it after the worker (
kick-bot-prod), selectbots:internalplus the account scopes it uses (see Provisioning bot and worker keys), store the secret in the secrets manager, configure the worker to sendAuthorization: Bearer <key>. - Provision the Discord bot - create a key in the same environment with the
bots:internalscope and set its one-time value asDISCORD_BOT__API__SYSTEM_KEYin the Discord bot service.System access requiredmeans the API did not accept a System key;Missing permission: bots:internalmeans the key is valid but lacks that grant. The API log names the first case:lm_sys_ key not found in config or database(the key belongs to another environment or was copied incompletely) orSystem key expired. - Rotate a leaked key - create a new key, update the consumer to use it, then revoke the old one.
- Audit inactive keys - scan the "Last used" line on each card and revoke keys with no recent usage. The list is not sortable.
Provisioning the apps/id audit-ingest key
The identity app (apps/id) writes user-scope security events (user:login, user:oauth_granted) to the Rust API via POST /v1/internal/audit-ingest, authenticated by a System key with the narrow audit:ingest grant. Without that key those events are silently dropped and apps/id's GET /api/health reports audit_ingest_configured=false. To provision it:
- Open System → System Keys and click Create Key.
- Label it for the environment, e.g.
apps-id-audit-ingest-prod. - In the scope picker, search for
audit:ingestand select only that scope - nothing else. A dedicated single-grant key keeps the blast radius of a leak to audit ingest alone. - Create the key and copy the plaintext
lm_sys_…value from the one-time reveal dialog. - Set it as
LUMIO_AUDIT_INGEST_KEYinapps/id's environment (seeapps/id/.env.example) and redeployapps/id. - Confirm
apps/id'sGET /api/healthnow reportsaudit_ingest_configured=true, then verify a fresh login produces auser:loginrow in that user's audit log.
Rotate it like any other system key: mint a replacement, update LUMIO_AUDIT_INGEST_KEY, redeploy, then revoke the old key.
Because the key carries only audit:ingest, it reaches no other System surface: every internal or cross-account System branch checks a named grant, never the key type alone - the bot-fleet surface, extension functions on any install, draft extension files and bundles, and the WebSocket presence:* / sounds:* channels all require bots:internal. What remains open to it is only what is open to any authenticated caller without a permission (for example the public-platform-data read emoteInfo / GET /v1/emotes/info, where a system key - having no account - gets no owner lookups).
Provisioning bot and worker keys
The bot fleet - twitch-bot, kick-bot, youtube-bot, discord-bot, bot-module-worker and automation-worker - authenticates with system keys and reads its internal feeds under bots:internal (ZAF-1922). Each of these services' keys must carry:
bots:internal, plus- whatever account scopes the service already uses on the regular account surface (for example
chat:*,events:*or moderation scopes the bot calls on behalf of an account).
A bot/worker key without bots:internal authenticates but is refused on every fleet feed (403 Missing permission: bots:internal on REST, the same Missing permission: bots:internal error on GraphQL). Scopes cannot be edited after creation, so an existing key that lacks the grant is fixed by minting a replacement that includes it, rolling it out to the service, and revoking the old key - an operator holding system-keys:create and system-keys:delete does this from this page. Keep one key per service and environment so each key's reach stays limited to what that service needs.
Permissions
| Action | Permission |
|---|---|
| View keys | system-keys:read |
| Create a key | system-keys:create |
| Revoke a key | system-keys:delete |
Dashboard entry requires admin:access. System admins implicitly hold every system-keys permission.
API
| UI action | GraphQL | REST |
|---|---|---|
| List system keys | adminSystemKeys | GET /v1/admin/system-keys |
| Create a system key | adminCreateSystemKey | POST /v1/admin/system-keys |
| Revoke a system key | adminDeleteSystemKey | DELETE /v1/admin/system-keys/\{id\} |
| List assignable scopes | adminSystemKeyPermissions | GET /v1/admin/system-key-permissions |
Tips & gotchas
- The plaintext key is shown once. The key card displays only the non-secret prefix afterwards; there is no way to recover the full key, so lost keys must be revoked and recreated.
- Keys are stored as a SHA-256 hash in the
api_keystable (the row is flaggedis_system = true, with no owning user or account). The DB never sees the plaintext after creation. - System keys are one of Lumio's authenticated token contexts. See Authentication for the public auth overview.
- Label is free-form, but putting the environment (
prod,staging) in the name helps auditing later. - Permissions are chosen at creation time and cannot be edited afterwards - there is no update endpoint. Changing a key's scope means creating a new key and revoking the old one.
- Revoke has no confirmation. One click deletes the key.
Related
- System Connections - Lumio's outbound OAuth credentials (shares the same page)
- OAuth Clients - public OAuth apps for third-party users