Audit Log
Overview
The personal log records email sign-in setup and confirmation, password changes and resets, 2FA activation/deactivation, authenticator app and passkey additions/removals, and recovery-code use, step-up confirmations, export requests/downloads and deletion scheduling/cancellation. Account events include member departure and subscription cancellation requests. The operator log records support-code reveal/check and manual 2FA reset. A code, password, link token or raw email address is never written to audit metadata.
The dashboard offers CSV/JSON export for the personal and account logs. The personal export is self-only; account export requires audit-log:read. A selected export accepts up to 1,000 event IDs. The regular GraphQL and REST readers remain paginated; file exports are REST-only.
The audit log is Lumio's tamper-evident record of security- and
administration-relevant actions. Every row in the audit_events table carries an
explicit scope column - user, account, or system - set by the writer
and never inferred at read time. Scope partitions the log into three audiences,
each with its own read surface, so a reader only ever sees the events meant for
them.
| Scope | Audience | Example events |
|---|---|---|
user | The individual user, self-only | Personal-security rows keyed to the authenticated user |
account | An account's members, gated to that account | Role/permission changes, member-role assignment, channel connections |
system | Platform operators | Instructed GDPR chat erasures (one per instructing channel) and platform-operator rows |
Scope isolation is enforced by each reader: the user reader returns only the
caller's scope=user rows, the account reader returns only the caller's account's
scope=account rows, and system rows are only available through the
operator-only admin view.
Overlap rule: an account-context action that has an actor lives only in the account log (where the actor is shown), never additionally in the user log; personal-security events live only in the user log. An event is written to exactly one scope.
Permission
Reading a single account's audit log requires the account-scope
audit-log:read permission. In the default roles it is held by Owner and
Administrator only - Moderator does not carry it.
This account permission shares its string with an admin-scope audit-log:read
that gates the operator/system log, but the two are distinct grants enforced
by different guards (the account guard checks the account role; the admin guard
checks the admin role). Holding the account permission never unlocks the system
log, and vice-versa. The self-service user log needs no permission at all - only
a first-party session.
Reading the log
Your personal log (user scope)
Your own scope=user rows. This is self-service - it needs no RBAC
permission - but it does require a first-party principal: a logged-in session
or your own API key. A popout, overlay, widget, or extension token cannot read
it. Current writers record successful logins, failed login attempts tied to a
known user, OAuth grant consent, and your personal API-key
(lm_usr_*) create/rename/revoke/rotate.
- GraphQL:
myAuditLog(filter: AuditLogFilter!): AuditLogPage! - REST:
GET /v1/me/audit-log
Your account's log (account scope)
Tenant actions on the active account. Current emitters write role and permission
changes, member-role assignment, channel-connection add/remove rows,
account-scoped GDPR erasure rows, and account service-key (lm_svc_*) lifecycle
(create / rename / revoke / rotate). Requires the account-scope audit-log:read
permission (Owner and Administrator by default). Returns only the active
account's rows.
- GraphQL:
accountAuditLog(filter: AuditLogFilter!): AuditLogPage! - REST:
GET /v1/account/audit-log
Operator log (system scope)
Operator and cross-account rows. This is the admin surface, gated by the
admin-scope audit-log:read; see Admin → Audit Log.
- GraphQL:
adminAuditLog(filter) - REST:
GET /v1/admin/audit-log
Filtering and pagination
Both the user and account readers share the same filter and paginated response shape, exposed identically on GraphQL and REST (same fields, permission, validation and errors).
| REST query param | GraphQL filter field | Meaning |
|---|---|---|
page | page | Page number (default 1). |
limit | limit | Page size (REST default 50, clamped 1..=200). |
type | eventType | Exact event-type match. |
from | dateFrom | Only events at/after this instant (RFC-3339; an unparseable value is ignored). |
to | dateTo | Only events at/before this instant (RFC-3339; an unparseable value is ignored). |
A page (AuditLogPage) carries items, total, page, and limit; the REST
response additionally includes total_pages.
The account log's Type filter includes account:discord_guild_connected, account:discord_guild_disconnected, account:discord_guild_bot_removed, and account:discord_guild_reconnected. The admin log offers the same four account event types.
Each AuditEvent exposes id, accountId, userId, eventType, scope,
ipAddress, userAgent, country, city, metadata (a JSON string),
createdAt, and resolvedNames. The country / city fields are filled by the
optional GeoIP service and are empty when it is disabled or cannot resolve the
client IP. userAgent carries the raw request User-Agent string and is
populated on every event that originates from a client request - not just
user:login* (ZAF-1412); the dashboard and admin viewers parse it to show the
device, browser, and operating system. It is empty only for events with no
originating request (background workers, the Stripe webhook) and for rows past
their retention window (see Retention, which nulls it in place).
Events written from the Shopify admin (/v1/shopify/embedded/*) carry the
user agent but never ipAddress, country or city: that hop is not trusted
to name a client IP, so shopify_user_id and shop_host in the metadata
identify who acted.
resolvedNames (REST resolved_names) is a read-time list of
{ id, kind, displayName } that resolves the entity UUIDs an event references —
the actor plus an allowlist of metadata ids (target_user_id, member_id,
from_account_id, to_account_id, target_account_id, role_id) - to their
display name (kind ∈ user | account | role). It is computed once per page
so the UI can show a name alongside every UUID, not just the actor. Deleted or
unknown entities are omitted from the list, so their raw UUID stays on the wire
and the UI falls back to it (ZAF-1233).
In the dashboard and admin viewers
The dashboard viewers (My activity, Account audit log) and the admin
operator viewer render resolvedNames as name + UUID together, never one
instead of the other (ZAF-1234):
- Actor column (account + operator logs): the acting user's display name followed by the short UUID; the full UUID is on hover.
- Device column: the raw
userAgentis parsed at render time into a compact device / browser / operating-system summary (e.g. "Chrome 141 · macOS"; text only in the dashboard tables, with a device icon in the admin operator log), alongside the existing IP address and Location columns. The parser never shows the raw value and never guesses: a user agent it cannot read at all — absent, a non-browser client (node,undici,axios), a legacy web-server string, or junk — renders as the localized Unknown ("Unbekannt" in German), and a missing IP renders the same way. Partial information is kept rather than dropped: a recognized browser with an unreadable OS shows "Chrome 141 · Unknown", and vice-versa. The full raw user-agent string remains available in the expanded row detail as the forensic source of truth. - Expanded row detail: above the raw metadata JSON, a resolved-entities list
annotates the actor and every allowlisted metadata id
(
target_user_id,member_id,from_account_id,to_account_id,target_account_id,role_id- e.g. thefrom/toaccounts of an active-account switch) with its resolved name. The raw JSON block is kept intact underneath. - Deleted / unknown entity: when an id is not in
resolvedNames(the entity was deleted or is unresolvable), the viewer shows the short UUID plus a localized "deleted user / account / role" label - never a blank.
Event catalog
Every audit event type is a wire string. Most catalog entries use
scope:action naming; the two GDPR erasure strings keep their historical
underscore names.
| Event type | Scope | Current writer |
|---|---|---|
chat_pii_erasure | account / system | account: account dissolution and admin account delete; system: admin chat-subject or single-record erasure on one channel's instruction, and the Lumio-only erasure |
youtube_member_erasure | system | Admin YouTube member-channel erasure on one channel's instruction |
account:role_created | account | Role creation |
account:role_updated | account | Role edits, including permission changes |
account:role_deleted | account | Custom role deletion |
account:member_role_assigned | account | Member role assignment. Metadata also counts the member's live personal API keys (personal_keys_affected) and popout tokens (popout_tokens_affected), which follow the new role from the next request; null when the count could not be read |
account:member_removed | account | A member was removed from the account (target user in metadata) |
account:member_left | account | A member left their own account; actor is that member. Metadata: membership_id |
account:invite_created | account | An account invite was created (invite email and code are never stored in metadata) |
account:invite_revoked | account | An account invite was revoked/deleted |
account:invite_accepted | account | An invite was accepted; actor is the joining user, scoped to the account joined. Bell acceptance records source: "notification" with membership_id, invite_id, and role_id; never the invite code or email |
account:invite_declined | account | The invitee declined from the bell and the account invite was deleted. Actor is the invitee; metadata invite_id, role_id, source: "notification". A missing invite is a no-op and writes no event |
account:subscription_cancel_requested | account | An account member requested cancellation of a subscription. Metadata: plan, period_end, cancellation kind, and has_reason; the reason itself is not stored |
account:dissolved | account | An account was irreversibly dissolved (CASCADE delete); actor is the owner. Metadata via: "immediate" (the owner dissolved without a grace period, graceDays: 0 included) or "scheduled" (the account security worker at the end of the grace period; actor is the owner who scheduled it), plus deleted_history_row_count (stream-history rows deleted) and deleted_object_count / pending_object_count (stored files deleted / left queued for retry) |
account:dissolution_scheduled | account | The owner scheduled the account's dissolution (scheduleAccountDissolution / POST /v1/accounts/{id}/dissolution); actor is the owner. Metadata: grace_days, scheduled_for |
account:dissolution_cancelled | account | The owner cancelled a scheduled dissolution (cancelAccountDissolution / DELETE /v1/accounts/{id}/dissolution); actor is the owner. Metadata: scheduled_for (the cancelled date) |
account:identity_changed | account | The channel's name/picture source, own name or own picture changed (setAccountIdentitySource / PUT /v1/accounts/{id}/identity-source, updateAccountName / updateAccount / PATCH /v1/accounts/{id}/name, POST /v1/accounts/{id}/avatar, removeAccountAvatar / DELETE /v1/accounts/{id}/avatar); actor is the caller. Metadata: changed (list of source, name, avatar; ["source", "avatar"] when switching back to a connection deleted an upload), source_kind, source_platform - never a name or URL. Not written when nothing changed |
account:connection_added | account | Channel OAuth completion on first connect and reconnect. Metadata: platform, credential_source, and, for a platform with optional channel scopes, optional_scopes_granted (the granted optional scope names, possibly []) — never tokens or the full grant |
account:connection_removed | account | Channel disconnect |
account:integration_connected | account | A Shopify store was linked: confirmed in Lumio (actor = that member) and completed in the Shopify admin. Metadata platform, shop_host, scopes, source (shopify_admin), shopify_user_id |
account:integration_disconnected | account | A Shopify store connection was removed: by a member (disconnectShopify / DELETE /v1/connections/shopify), from the Shopify admin (no actor, + shopify_user_id) or by Shopify's app/uninstalled webhook (no actor). Metadata platform, shop_host, reason (user / shopify_admin / app_uninstalled) |
account:shopify_settings_updated | account | A Shopify setting (customer_display, show_items or show_price; one event per changed value) changed: from Lumio (actor = the member, source: lumio) or from the Shopify admin (no actor, source: shopify_admin + shopify_user_id). Metadata platform, shop_host, setting, from, to, source |
account:shopify_order_name_hidden | account | A member hid the buyer name of one Shopify order (both of its events). Metadata platform, event_id; never the name |
account:shopify_data_request | account | Shopify forwarded a customer's data request for the account's store; the owner got an in-app notice (no actor). Metadata platform, shop_host, orders_requested, orders_found, orders_with_label (counts only) |
account:shopify_customer_redacted | account | The listed orders' events were deleted (customers/redact, no actor), after the second pass. Metadata platform, shop_host, orders (count) |
account:shopify_shop_redacted | account | The store's events were deleted up to the uninstall (shop/redact, no actor), after the second pass. Metadata platform, shop_host |
account:youtube_revenue_data_deleted | account | Stored YouTube Analytics revenue rows were erased after disconnect, scope loss, revocation, an unconfirmed authorization, a deleted video, a deleted stream report or account deletion. Metadata: platform: "youtube", reason, rows (deleted row count); never an amount |
account:discord_guild_connected | account | A Discord server was connected to the account through the verified invite of the Lumio bot (Connections → Discord). Actor = the inviting member. Also written for the new account when it takes over a server whose bot had been removed from another account (reason: taken_over). Metadata: guild_id, guild_name - never the invite's user token |
account:discord_guild_disconnected | account | A Discord server was disconnected (disconnectDiscordGuild / DELETE /v1/discord-guilds/{guild_id}, bot-connections:delete). Also written, without an actor, for the previous account of a takeover (reason: taken_over). Metadata: guild_id, guild_name |
account:discord_guild_bot_removed | account | The Lumio bot was removed from a connected Discord server on Discord's side. No actor. The connection is marked, not deleted, and keeps its settings until a reconnect or the 90-day cleanup. Metadata: guild_id, guild_name |
account:discord_guild_reconnected | account | A Discord server already connected to the account was invited again (after the bot was removed, or to grant missing permissions); its settings stay. Actor = the inviting member. Metadata: guild_id, guild_name |
account:extension_kill_switch_set | account | The extension kill switch was set or cleared (POST /v1/bot-modules/kill / setBotModuleKillSwitch, bot-modules:edit). Metadata: killed only |
account:extension_moderation | account | An installed extension deleted a chat message, or banned, unbanned or timed out a chat user: a widget or app through useLumioAction (chat:delete / chat:ban / chat:unban / chat:timeout, REST POST /v1/extension-installs/{install_id}/actions/{action} or GraphQL executeExtensionAction), or a bot module through its handler actions or moderation decisions (chat:ban / chat:unban / chat:timeout; a bot module's message deletions are recorded in its install log only, not here). The account's bot carried it out; written only when the platform applied it. No actor user; no origin for bot modules. Metadata: source (extension or bot_module), extension_id, install_id, action, platform, target_user_id (none for delete), duration_secs (timeout only) - never the reason text |
account:extension_uninstalled | account | An extension was uninstalled (DELETE /v1/extension-installs/{id} / uninstallExtension, extension-store:uninstall). Metadata: extension_id, install_id, delete_data_immediately (true = the account's extension data was deleted at once, false = 30-day retention) |
account:connection_reconnect_required | account | A channel/bot token is dead: a refresh failed terminally (Token Refresh Worker), or a platform worker's forced refresh after an HTTP 401 could not produce an accepted token (Twitch EventSub, Kick subscriptions); the connection was auto-flagged reconnect_required |
account:connection_credentials_upserted | account | Per-account OAuth app credentials (client_id/secret) created/updated via saveAppCredentials / PUT /v1/connections/credentials/{platform} (connections:create); the credential level, distinct from account:connection_added (token level). Metadata: platform, credentials_id, updated - never the client_id/client_secret |
account:connection_credentials_deleted | account | Per-account OAuth app credentials removed via deleteAppCredentials / DELETE /v1/connections/credentials/{platform} (connections:delete); the DELETE counterpart of account:connection_credentials_upserted. The same call also disconnects the channel, which emits account:connection_removed. Fires only on a real removal. Metadata: platform only - never the client_id/client_secret |
account:webhook_target_blocked | account | A manual automation run (executeAutomation / POST /v1/automations/{id}/execute, automations:execute) reached a webhook_call node whose target the webhook target filter refused. Actor = the member who ran it. Metadata: automation_id, reason (invalid_url, scheme_not_allowed, private_target), scheme - never the host, path or query. Event- and timer-triggered runs do not write it |
public_stats:channel_opted_out | account | Broadcaster hides a channel from the public Stats app (is_public → false) |
public_stats:channel_opted_in | account | Broadcaster re-lists a channel on the public Stats app (is_public → true) |
billing:withdrawal_declared / billing:cancellation_declared | system for public; account or user for signed-in withdrawal | A declaration was stored. Public cases have no authenticated actor. Signed-in withdrawal uses account scope with an active channel, user scope otherwise. Cancellation of a selected plan or extension uses its subscription-specific request event. |
account:subscription_cancel_requested / extension:subscription_cancel_requested | account | A signed-in member requested cancellation of a plan or paid extension subscription; actor and declaration ID are recorded without the free-text reason. |
billing:withdrawal_processed / billing:cancellation_processed | system | An operator closed a declaration as processed or rejected. The event records the decision and case ID, not the declarant's personal details. |
billing:declaration_matched / billing:declaration_retention_extended | system | An operator matched a public case to a contract or extended its retention date. |
billing:withdrawal_waiver_accepted | account | A paid Stripe checkout (plan or extension) completed after the buyer ticked the withdrawal-waiver declaration (Billing). Actor = the buyer. Written once per checkout session by the Stripe webhooks. Metadata: kind (plan, extension_payment, extension_subscription), text_version, accepted_at, checkout_session_id, plan_id or extension_id |
account:popout_token_created / account:popout_token_updated / account:popout_token_deleted | account | Popout-token lifecycle (create / update / delete). A soft-revoke via update carries "revoked":true; an update that sets or changes the user binding carries bound_user_id (opaque uuid, ZAF-1922). Otherwise metadata is the non-secret token_id + lm_pop_* prefix only |
account:overlay_token_rotated / account:overlay_token_revoked | account | Overlay access-token rotate / revoke. Metadata carries overlay_id + token ids + the lm_overlay_* prefix; never the token string |
account:shared_link_created / account:shared_link_revoked / account:shared_link_extended | account | Shared-overlay link (lm_share_*) lifecycle; create/revoke carry the lm_share_* prefix, extend carries the new expiry. Never the token string/hash |
account:widget_token_created / account:widget_token_rotated / account:widget_token_revoked | account | Widget access-token lifecycle (create / duplicate / rotate / revoke). Metadata is widget_id only - the token id/prefix are not surfaced by the helpers and the full token is never stored |
account:overlay_access_granted / account:overlay_access_revoked | account | Per-overlay access grant (upsert - resulting role in metadata) / revoke; target_user_id in metadata |
account:widget_access_granted / account:widget_access_revoked | account | Per-widget access grant (upsert - resulting role in metadata) / revoke; target_user_id in metadata |
account:history_shared_link_created / account:history_shared_link_revoked / account:history_shared_link_extended | account | Stream-history report share link (lm_share_*) lifecycle; create/revoke carry the lm_share_* prefix, extend carries the new expiry. Metadata includes session_id, link_id, and effective non-secret options (legacy links identify their legacy view); never the token string/hash or the argon2 password_hash |
account:bot_list_updated | account | A platform account was marked or unmarked as a bot through GraphQL or REST. Metadata: action (added/removed), platform, platform_user_id, display_name, source. Duplicate adds and missing removals emit nothing. |
account:history_keywords_updated | account | A tracked keyword was added, changed or removed through GraphQL or REST. Metadata: action (added/changed/removed), keyword_id, keyword, match, case_insensitive. Unchanged writes emit nothing. |
account:history_revenue_settings_updated | account | Revenue assumptions changed through GraphQL or REST; actor is the user. Metadata lists the changed field names (changed_fields), never their values; any added, removed or repriced YouTube membership level appears as youtube_member_levels. Reordering levels alone and unchanged writes emit no event; fixed USD values are never edited |
account:history_session_deleted | account | A stored stream-history session is deleted (GDPR Art. 17) via deleteChannelHistory / DELETE /v1/history/reports/{id}; session_id in metadata |
account:setting_preset_shared / account:setting_preset_unshared | account | A user shares/un-shares one of their setting presets with an account (shareSettingPreset / unshareSettingPreset + POST / DELETE /v1/setting-presets/{id}/share, presets:share). Shared keys the account it is shared into; un-shared keys the account it was shared into (fires only on a real removal). Metadata preset_id, kind, name - never the preset payload (it can hold viewer names) |
account:login_connection_assigned | account | An account member's login connection was routed to the account for a provider (assignLoginConnection / POST /v1/accounts/login-assignments, login-assignments:create on the cross-user path); provider + target_user_id + connection_id in metadata |
account:login_connection_removed | account | An account member's login assignment was removed for a provider (removeLoginAssignment / DELETE /v1/accounts/login-assignments/{provider}, login-assignments:delete); provider + target_user_id in metadata |
account:extension_access_granted / account:extension_access_revoked | account | An extension owner grants/revokes another account's direct access to their extension (grantExtensionAccess / revokeExtensionAccess + POST / DELETE /v1/developer/extensions/{id}/access[/{grant_id}], extension-dev:edit + ownership). Row account_id = the extension-owner (acting) account; extension_id, target_account_id, grant_type, grant_id in metadata |
account:extension_access_invite_created / account:extension_access_invite_revoked / account:extension_access_invite_accepted | account | Extension-access invite lifecycle ({create,delete}ExtensionAccessInvite / acceptExtensionAccessInvite + POST / DELETE / .../accept REST). Metadata extension_id, invite_id, grant_type, grant_id, max_uses, expires_at - never the invite_code/code |
account:obs_config_saved / account:obs_config_deleted | account | The OBS WebSocket config is saved (created or changed) or removed (saveObsConfig / deleteObsConfig + PUT / DELETE /v1/integrations/obs, obs:{edit,delete} + feature:obs_remote + integration:obs_websocket); deleted is written only when a config was actually removed. Metadata mode (local / remote), remote_host, port, has_password - never the password or its ciphertext |
account:se_token_created / account:se_token_deleted | account | A StreamElements token is created/upserted or deleted (createSeToken / deleteSeToken + POST / DELETE /v1/se-tokens[/{id}], se-tokens:{create,delete} + feature:streamelements); metadata token_id, platform, label - never the JWT, its ciphertext, or the masked token_hint |
account:service_key_created / account:service_key_updated / account:service_key_revoked / account:service_key_rotated | account | An account service key (lm_svc_*) is minted, renamed, revoked, or rotated by an account member (createAccountServiceKey/updateAccountServiceKey/deleteAccountServiceKey/rotateAccountServiceKey + REST POST/PATCH/DELETE /v1/service-keys / POST /v1/service-keys/{id}/rotate, apikeys:* + feature:apikeys). Account-scoped because the key is a tenant credential that outlives its creator; metadata service_key_id + key_prefix (or old_key_prefix+new_key_prefix on rotate) - never the key or its hash |
user:api_key_created / user:api_key_updated / user:api_key_revoked / user:api_key_rotated | user | A member mints, renames, revokes, or rotates the secret of a personal lm_usr_* API key; emitted by GraphQL createUserApiKey/updateUserApiKey/deleteUserApiKey/rotateUserApiKey and REST POST/PATCH/DELETE /v1/api-keys / POST /v1/api-keys/{id}/rotate (metadata is api_key_id + key_prefix, or old_key_prefix+new_key_prefix on rotate - never the key or its hash) |
user:developer_api_key_created / user:developer_api_key_updated / user:developer_api_key_revoked | user | A developer mints, renames, or revokes a lm_dev_* developer API key; emitted by GraphQL createDeveloperApiKey/updateDeveloperApiKey/revokeDeveloperApiKey and REST POST/PATCH/DELETE /v1/developer/api-keys. The acting developer is the subject - the key is bound to a developer profile/team that has no account, so this is a user-scope event (there is no account log to mirror it into). Metadata carries the non-secret developer_api_key_id + key_prefix (and owning developer/team id), never the key or its hash |
user:login | user | Successful login - native (PKCE) redemption emits in-process from the Rust /auth/token/exchange handler (method: pkce) and the GraphQL exchangeToken mutation emits inline (method: graphql_exchange, its only success writer - no first-party caller for apps/id ingest to cover); the web flow is emitted by apps/id via audit-ingest |
user:login_failed | user | Rejected login - the Rust REST /auth handlers and the GraphQL exchangeToken mutation (at parity) emit on failed provider-token validation or a PKCE mismatch; the subject is the resolved user, or NULL when the credential maps to no known Lumio user |
user:oauth_granted | user | Provider link/reconnect consent by an authenticated user (emitted by apps/id via audit-ingest) |
user:login_reconnect_required | user | A login-connection token refresh failed terminally; the Token Refresh Worker auto-flagged reconnect_required |
user:active_account_switched | user | The user switched or cleared their session's active account (updateMe / PATCH /v1/users/me); from/to account ids in metadata, account_id NULL so it stays out of the target account's tenant log |
user:logout | user | The user ended their own session - GraphQL logout / logoutSession, REST POST /v1/auth/logout. Metadata: ended session_id (when known) + method (refresh_token / session) |
user:session_revoked | user | The user revoked session(s) from session management - GraphQL deleteSession / deleteAllOtherSessions, REST DELETE /v1/users/me/sessions/{id} / /sessions. Metadata distinguishes single (session_id, revoked_all:false) from bulk (count, revoked_all:true) |
user:email_login_requested | user | A user requested or resent an email sign-in verification link. Metadata: purpose and resend, never the address or link |
user:email_login_added | user | Email sign-in was enabled after registration or dashboard setup. Metadata: via (register or dashboard) |
user:email_login_changed | user | A user completed an email sign-in address change; metadata is empty |
user:email_login_cancelled | user | A user cancelled pending email sign-in setup or change; metadata is empty |
user:email_verified | user | An email address was verified during registration, sign-in setup or address change. Metadata: purpose (register, add or change), never the address |
user:login_method_removed | user | A user removed email sign-in. Metadata: method (email) |
user:password_changed | user | A user changed their password; metadata is empty |
user:password_reset_requested | user | A password reset was requested for a resolved user; metadata is empty |
user:password_reset_completed | user | A user completed a password reset; metadata is empty |
user:email_changed | user | The acting user's profile email changed - through setting up or removing the email sign-in, or removing a platform address - an account-takeover vector; metadata carries only had_previous_email and the source or cleared flag, never the address itself |
user:profile_identity_changed | user | The user changed their own profile identity (ZAF-2096): switched the name/picture source between a sign-in connection and their own details (GraphQL setNameSourceConnection, REST PUT /v1/users/me/name-source-connection), changed the own display name (updateDisplayName / PATCH /v1/users/me/display-name), uploaded an own picture (REST-only POST /v1/users/me/avatar) or removed it (removeAvatar / DELETE /v1/users/me/avatar). Metadata: changed (list of source, display_name, avatar) and, for a source switch, source = own / connection - never a name or picture URL. Choosing among one's own connections is not audited |
user:data_export_requested | user | A user requested their own data export. Metadata: export_id, export model, account_ids, and include_chat; never export contents |
user:data_export_downloaded | user | A user downloaded their own export. Metadata: export_id and model; never export contents |
user:deletion_scheduled | user | A user scheduled deletion of their own profile. Metadata: grace_days and scheduled_for |
user:deletion_cancelled | user | A user cancelled their scheduled profile deletion; metadata is empty |
user:data_export_cancelled | user | The user cancelled one of their own data exports while it was still pending or running - GraphQL cancelDataExport, REST POST /v1/users/me/data-exports/{id}/cancel. Metadata: export_id + model, never export contents |
user:login_connection_removed | user | A user removed one of their own login connections (disconnectLoginConnection / DELETE /v1/login-connections/{id}, first-party gated ZAF-469); provider + connection_id in metadata |
developer:verification_submitted | user | An approved developer submitted or resubmitted their own KYC verification (submitDeveloperVerification / POST /v1/developer/verification, first-party gated ZAF-469); verification_id, developer_id, developer_type, status, resubmission in metadata - never the submitted KYC/PII fields |
developer:tax_details_updated | user | A developer set the tax details of their own verification (Steuernummer, USt-IdNr., § 19 UStG answer; updateDeveloperTaxDetails / PUT /v1/developer/verification/tax-details). Metadata says which details are present, never their values |
developer:payout_requested | user | A developer requested a payout of their balance (requestPayout / POST /v1/developer/payouts/request), or the closing payout of their profile deletion was created; source request | closing. Metadata: payout id, method, amount, status and the masked destination - never the full PayPal e-mail or IBAN |
developer:extension_secret_set | user | A developer created or replaced an extension secret (setExtensionSecret / PUT /v1/developer/extensions/{id}/secrets/{key}). extension_id and the secret's key name in metadata, never the value; developer_key_id instead of an actor for a developer key |
developer:extension_secret_deleted | user | A developer deleted an extension secret (deleteExtensionSecret / DELETE …/secrets/{key}); only when a secret was actually removed. Same metadata as developer:extension_secret_set |
developer:extension_visibility_changed | user | A developer changed their extension's store visibility (updateExtension / PATCH /v1/developer/extensions/{id}); emits only on a real change. extension_id, from, to in metadata; a change made with a developer key (lm_dev_*) has no actor and carries developer_key_id instead |
developer:team_created / developer:team_deleted | user | A developer creates/deletes their own extension-developer team (create/deleteDeveloperTeam + POST / DELETE /v1/developer/teams[/{id}]). Self-only (myAuditLog); team_id (+ slug on create) in metadata - never the team name PII |
developer:team_role_created / developer:team_role_updated / developer:team_role_deleted | user | Developer-team custom-role lifecycle ({create,update,delete}DeveloperTeamRole + POST / PATCH / DELETE /v1/developer/teams/{id}/roles[/{rid}], team-settings:edit); team_id, role_id, role_slug, permissions, changed_fields in metadata |
developer:team_member_role_changed / developer:team_member_removed | user | A developer changes a team member's role or removes them (updateDeveloperTeamMemberRole / removeDeveloperTeamMember + REST twins, team-members:{edit,remove}); team_id, member_id, target_user_id, role_id in metadata. The acting developer's self-only log records it (§A.1) |
developer:team_invite_created / developer:team_invite_revoked / developer:team_invite_accepted | user | Developer-team invite lifecycle. Acceptance by code or from the bell records team_id, invite_id, role_id, member_id; bell acceptance also records source: "notification". Creation and revocation record their applicable IDs, max_uses, and expires_at. Never the invite email or code |
developer:team_invite_declined | user | The invitee declined a developer-team invite from the bell. Metadata team_id, invite_id, invite_deleted and source: "notification". An unused single-use invite is deleted; a shared or used invite remains. A missing invite writes no event |
user:mfa_enabled / user:mfa_disabled | user | A user enabled or disabled two-factor authentication. Metadata names the method (totp or passkey); the first method emits user:mfa_enabled |
user:mfa_factor_added / user:mfa_factor_removed | user | A further authenticator app or passkey was added, or a method was removed while another remains. Metadata contains method, factor_id and the resulting factor_count; additions may include aaguid, removals trusted_devices_revoked. The user-chosen label is excluded. |
user:mfa_challenge_failed | user | A two-factor challenge failed. Metadata: challenge context |
user:mfa_recovery_code_used | user | A recovery code was used. Metadata: context and codes remaining |
user:mfa_recovery_codes_regenerated | user | A user regenerated their recovery codes; metadata is empty |
user:mfa_trusted_device_revoked | user | A user revoked one or all trusted devices. Metadata: device_id, all, and affected count |
user:step_up_verified | user | A user confirmed a sensitive action with step-up authentication. Metadata: verification method |
crawler:scope_changed | system | A platform operator changes the crawler scope |
crawler:watchlist_added | system | A platform operator adds a channel to the crawler watchlist |
crawler:watchlist_removed | system | A platform operator removes a channel from the crawler watchlist |
copyright:moderation_approved / copyright:moderation_dismissed | system | A platform operator (copyright:moderate) approves a community candidate into the global safe or blocked list, or dismisses a report. REST vote-candidate actions and GraphQL approveReport / dismissReport emit the same types after a real decision. Metadata: spotify_track_id, list (blocked, safe, or null), source, decision (approved or dismissed); no song title, artist, or reporter identity. Dismissal emits only when a report was dismissed |
system:support_code_viewed | system | A platform operator revealed a user's support code. Metadata: target_user_id, never the code |
system:support_code_checked | system | A platform operator checked a user's support code. Metadata: target_user_id and result (match or no_match), never the code |
system:user_mfa_reset | system | A platform operator reset a user's two-factor authentication. Metadata: target_user_id and required operator reason |
system:feature_flag_updated | system | Admin global feature-flag toggles and platform provider kill-switches |
system:plan_created / system:plan_updated / system:plan_deleted | system | Admin plan lifecycle (create / edit / delete) |
system:plan_feature_updated | system | Admin plan↔feature mapping changes |
system:account_plan_changed | system | An account's plan changed - an admin reassignment (operator path), or a Stripe webhook (checkout.session.completed / customer.subscription.updated / customer.subscription.deleted, discriminated by "via":"stripe" in metadata, actor NULL) |
system:account_feature_override | system | Admin sets or clears a per-account feature override |
system:account_limits_updated | system | A platform operator changes an account's numeric limit overrides (account_limits), incl. the setting-preset quota, via either write path (adminUpsertAccountLimits / PUT /v1/admin/accounts/{id}/limits, or adminSetAccountLimits / PUT /v1/admin/accounts/{id}/resolved-limits, accounts:edit); account_id, a path discriminator (upsert|resolved) and the submitted limit fields in metadata - never the free-text notes |
system:user_setting_preset_deleted | system | A platform operator deletes one of a user's setting presets (adminDeleteUserSettingPreset / DELETE /v1/admin/setting-presets/{id}, users:edit); target_user_id, preset_id, kind in metadata - never the preset payload; only on a real deletion |
system:account_deleted | system | A platform operator deleted an account (DELETE /v1/admin/accounts/{id} / adminDeleteAccount); the deleted account id, "reason":"operator_delete", deleted_history_row_count (stream-history rows deleted) and deleted_object_count / pending_object_count (stored files deleted / left queued for retry) ride in metadata. System-scoped because the CASCADE leaves no account member to read an account-scoped row (owner self-service dissolve stays account:dissolved) |
system:orphaned_storage_swept | system | A platform operator applied the orphaned extension upload sweep (POST /v1/admin/storage/orphaned-extension-uploads/sweep / adminSweepOrphanedExtensionUploads, apply: true, admin:privacy-erase): extension upload files no row references were queued for deletion; kind, orphaned_object_count, queued_object_count in metadata. A dry run is not recorded |
extension:purchase_auto_refunded | system | The Stripe checkout.session.completed webhook refunded a paid extension checkout in full because it cannot be delivered: the extension or its version was deleted or unpublished while the checkout was open (cause: "detached"), or the account can no longer install it (cause: "not_installable"). No actor. purchase_id, cause, amount, currency and stripe_refund_id in metadata - no buyer data. Recorded only when Stripe accepted the refund |
privacy:chatter_objection_added | system | A platform operator records a chatter's GDPR Art. 21 objection (POST /v1/admin/privacy/chat/objections / addChatterObjection, admin:privacy-objection-create); platform, "reason":"objection" and already_erased in metadata - never the user id or login |
privacy:chatter_objection_removed | system | A platform operator lifts a chatter objection (DELETE /v1/admin/privacy/chat/objections/{platform}/{user_id} / removeChatterObjection, admin:privacy-objection-delete); same metadata shape. A failed lift records nothing |
privacy:channel_suppression_withdrawn | system | A platform operator withdraws a channel's erasure instruction for one chatter (POST /v1/admin/privacy/chat/erase/withdraw / withdrawChannelChatterSuppression, admin:privacy-erase-withdraw); target: "account", the account_id, the instruction_reference and the platform ride in metadata, never the user id or login. A failed withdrawal (no entry, unknown account, or a 409 for an erasure the person asked for) records nothing |
system:system_key_created | system | Admin mints a system API key (is_system=true); the permission set is registry-validated and a bounded expires_at is enforced |
system:system_key_revoked | system | Admin revokes a system API key (only the key id in metadata; never full_key/key_hash) |
system:connection_upserted | system | Admin creates/updates a system OAuth (login-provider) connection; the raw client_secret and its AES-256-GCM ciphertext are never stored |
system:connection_deleted | system | Admin deletes a system OAuth connection |
system:user_role_created / system:user_role_updated / system:user_role_deleted | system | Admin cross-account user-role lifecycle (create / edit / delete) |
system:user_role_assigned / system:user_role_unassigned | system | Admin assigns/removes a cross-account user role for a user (target user in metadata) |
system:user_permission_override_set / system:user_permission_override_removed | system | Admin sets/removes a per-user permission override (target user in metadata) |
system:account_permission_override_set / system:account_permission_override_removed | system | A platform operator (accounts:overrides-edit) sets/removes an account-level permission override (PUT / DELETE /v1/admin/accounts/{id}/permission-overrides / adminSetAccountPermissionOverride / adminRemoveAccountPermissionOverride); target account_id + permission (+ granted/reason on set) in metadata |
oauth_client:created / oauth_client:updated / oauth_client:deleted | system | Operator developer-application OAuth client lifecycle (oauth-clients:{create,edit,delete}); metadata client_id_ref (DB row id, not the OAuth client_id string), name, redirect_uris, scopes, changed_fields (update) - never the OAuth client_id/client_secret |
admin_role:created / admin_role:updated / admin_role:deleted | system | Operator admin-RBAC role lifecycle (admin-roles:{create,edit,delete}) on the admin_roles table (distinct from user_roles); metadata role_id, role_name, permissions, changed_fields (update) |
admin_role:assigned / admin_role:unassigned | system | Operator assigns/removes an admin role for a user (admin-roles:edit); metadata role_id + target_user_id. Emits only on a real (non-idempotent) change |
system:global_bot_connection_upserted / system:global_bot_connection_deleted | system | Global (nil-UUID) bot connection lifecycle (bot-connections:{create,delete}): OAuth exchange upsert and delete. Metadata platform, connection_id, bot_username, source (oauth_exchange) - never the access/refresh token. The OAuth-exchange path is REST-only |
system:user_deleted | system | A platform operator deleted a user and all their data (users:delete, DELETE /v1/admin/users/{id} / adminDeleteUser); target_user_id in metadata |
system:user_login_connection_deleted | system | A platform operator removed one of a user's login identities (adminDeleteUserLoginConnection / DELETE /v1/admin/users/{id}/login-connections/{provider}); target_user_id + provider in metadata |
developer:funds_deletion_released | system | A platform operator released a developer balance Lumio could not pay out (no payout method, identity not verified) as a liability and deleted the user or profile (adminReleaseFundsAndDeleteUser / adminReleaseFundsAndDeleteDeveloper + POST /v1/admin/{users,developers}/{id}/release-funds-delete, developer-funds:release-deletion); released_balance_id, amount, currency, release_case in metadata, never the claim code, reason or request reference |
developer:funds_released_claim_started | system | A platform operator recorded that a person presented the claim code of a released developer balance (adminStartReleasedBalanceClaim + POST /v1/admin/released-balances/claim, developer-funds:settle); emitted once per record; released_balance_id, amount, currency in metadata, never the claim code |
developer:funds_released_settled | system | A platform operator credited a released developer balance to the person's new, verified developer profile (adminSettleReleasedBalance + POST /v1/admin/released-balances/settle, developer-funds:settle); released_balance_id, developer_id (the new profile), amount, currency in metadata, never the claim code |
system:developer_deleted / system:developer_team_deleted | system | A platform operator deletes a developer or a developer team (adminDeleteDeveloper / adminDeleteDeveloperTeam + DELETE /v1/admin/developers/{id} / DELETE /v1/admin/developer-teams/{id}, developer-verification:edit / developer-limits:edit); developer_id / team_id in metadata |
system:developer_team_member_added / system:developer_team_member_removed / system:developer_team_member_role_changed | system | Operator developer-team membership admin (admin{Add,Remove}TeamMember / adminUpdateTeamMemberRole + REST twins, developer-limits:edit); team_id, member_id/target_user_id, role_id in metadata |
system:developer_revenue_split_set | system | A platform operator sets a developer's revenue split (setDeveloperRevenueSplit / PATCH /v1/admin/developers/{id}/revenue-split, developer-limits:edit); developer_id + split in metadata |
system:developer_limits_updated | system | A platform operator updates a developer OR extension limit - one merged event discriminated by limit_scope ("developer" | "extension") in metadata (adminSetDeveloperLimit / adminSetExtensionLimits + PUT /v1/admin/developers/{id}/limits / PATCH /v1/admin/extensions/{id}/limits, developer-limits:edit); developer_id/extension_id, limit_key/limit_value or changed_fields in metadata |
system:developer_limit_request_reviewed | system | A platform operator reviews (approve/deny) a developer limit request (adminReviewLimitRequest / PUT /v1/admin/limit-requests/{id}, developer-limits:edit); request_id, status, developer_id, extension_id, limit_key, has_review_notes (flag) in metadata - never the free-text review_notes |
system:user_email_changed | system | A platform operator changes a user's email (adminUpdateUser / PATCH /v1/admin/users/{id}, users:edit); emitted only when the stored email actually changed; target_user_id + had_previous_email (flag) in metadata - never the address. Distinct from the user-self user:email_changed |
system:user_account_creation_override_set | system | A platform operator sets a user's account-creation override (adminUpdateUserAccountCreationOverride / PATCH /v1/admin/users/{id}, users:edit); target_user_id + override (allow|deny|default) in metadata |
system:user_max_accounts_override_set | system | A platform operator sets a user's max-accounts override (adminUpdateUserMaxAccountsOverride, users:edit) - GraphQL-only (no REST twin accepts max_accounts); target_user_id + max_accounts (int|null) in metadata |
system:account_connection_deleted / system:account_bot_connection_deleted | system | A platform operator deletes an account's channel / bot connection (adminDeleteAccountChannelConnection / _bot_connection + DELETE /v1/admin/accounts/{id}/connections/{platform} / .../bot-connections/{platform}, accounts:edit); target_account_id + platform in metadata; only on a real deletion. adminDeleteDiscordGuild / DELETE /v1/admin/discord-guilds/{guild_id} (discord-guilds:delete) writes system:account_bot_connection_deleted with platform: discord + guild_id |
system:account_login_connection_deleted | system | A platform operator deletes an account's login (identity) connection (adminDeleteAccountLoginConnection, accounts:edit) - GraphQL-only (no REST twin for the account-level delete); target_account_id + provider in metadata; only on a real deletion |
system:account_member_added / system:account_member_removed | system | A platform operator adds a user to / removes a member from an account (adminAddAccountMember / adminRemoveAccountMember + POST /v1/admin/accounts/{id}/members / DELETE /v1/admin/accounts/{id}/members/{user_id}, accounts:edit); target_account_id, target_user_id (+ role_id on add) in metadata; add only on a real insert (re-adding an existing member emits nothing), remove only on a real removal. Distinct from the tenant account:member_removed |
system:coupon_created / system:coupon_updated / system:coupon_deactivated | system | A platform operator creates, updates or deactivates a coupon (adminCreateCoupon / adminUpdateCoupon / adminDeactivateCoupon + POST / PUT / DELETE /v1/admin/coupons[/{id}], coupons:{create,edit}); coupon_id + discount_type (create) or changed_fields (update, names only) in metadata - never the redeemable code; deactivate only on a real deactivation |
system:account_se_token_deleted | system | A platform operator deletes an account's StreamElements token (adminDeleteSeToken / DELETE /v1/admin/accounts/{id}/se-tokens/{token_id}, se-tokens:delete); target_account_id + token_id in metadata - never the JWT or its hint. Distinct from the tenant account:se_token_deleted |
system:developer_verification_approved / system:developer_verification_rejected | system | A platform operator approves or rejects a developer KYC verification (approveDeveloperVerification / rejectDeveloperVerification + POST /v1/admin/developer-verifications/{id}/{approve,reject}, developer-verification:edit); verification_id (+ target_account_id on approve) in metadata - never the free-text rejection reason |
system:developer_credit_note_cancelled | system | A platform operator records a developer's objection to a self-billed credit note or corrects it (cancelCreditNote / POST /v1/admin/credit-notes/{id}/cancel, billing:edit). Metadata: note and cancellation ids and numbers, reason, developer id, re-issued note - never the supplier's details |
system:developer_payout_scheduled / system:developer_payout_sent / system:developer_payout_completed / system:developer_payout_failed | system | The developer payout lifecycle (ZAF-1257): the monthly cycle created a payout, the cycle handed it to Stripe/PayPal, it reached the developer (operator completeDeveloperPayout / POST /v1/admin/developer-payouts/{id}/complete, billing:edit, or the PayPal webhook), or it failed (provider refusal, operator failDeveloperPayout / …/fail, or the PayPal webhook; balance credited back). user_id is the operator, none for the cycle and the webhook; source names the path. Metadata as for developer:payout_requested, plus provider ids and, on failure, refunded and cancellation_number |
system:extension_version_approved / system:extension_version_rejected | system | A platform operator approves or rejects an extension version in review (approveExtensionVersion / rejectExtensionVersion + POST /v1/extensions/reviews/{id}/{approve,reject}, extension-review:edit); version_id, extension_id (+ new_status testing|published on approve) in metadata - never the review notes or reason |
system:user_extension_dev_mode_set | system | A platform operator toggles a user's extension dev mode (setExtensionDevMode / POST /v1/admin/users/{id}/extension-dev-mode, developer-limits:edit); target_user_id + enabled in metadata |
system:api_key_updated / system:api_key_rotated / system:api_key_revoked | system | A platform operator edits (label + scopes), rotates the secret of, or revokes any account's/user's API key from the admin app (adminUpdateApiKey/adminRotateApiKey/adminDeleteApiKey + REST PATCH / POST .../rotate / DELETE /v1/admin/api-keys/{kind}/{id}, account-apikeys:{edit,rotate,delete}). Operator is the actor; metadata kind (personal|service), api_key_id, target account_id, target user_id (personal only), plus permissions (update) or old_key_prefix+new_key_prefix (rotate) - never the key. Never surfaces in the target account's/user's own log |
The two rows the ID app contributes over POST /v1/internal/audit-ingest - the
web-flow user:login and user:oauth_granted - are written once
LUMIO_AUDIT_INGEST_KEY holds an lm_sys_* System key carrying the
audit:ingest grant. That key is mintable from the admin app (System →
System-API-Schlüssel), since audit:ingest is part of the System-key
assignable-scope registry. Without the key the web-login and OAuth-consent rows
are absent, while the native/PKCE and GraphQL user:login writers are
unaffected. See
Audit Events → Events that originate outside the API.
Storage
Audit events are persisted in TimescaleDB. The event catalog above lists every recorded type. See Admin → Audit Log for the operator view, a selection of operator-relevant events, and GeoIP location enrichment.
For the web dashboard surfaces, see Audit Log.
Retention
Audit events follow a fixed, three-step window, counted from created_at:
| Age | What is kept |
|---|---|
| 0–12 months | the full row |
| 12–24 months | the row without its network fields - ip_address, user_agent, country and city are nulled in place; account_id, user_id, type, metadata, scope and created_at remain |
| after 24 months | nothing - the row is deleted |
The erasure-proof types youtube_member_erasure and chat_pii_erasure are the
exception: they prove that a GDPR erasure was carried out, so they are deleted
after 36 months instead of 24. They carry no network fields, so the
12-month step changes nothing for them.
Both steps are native TimescaleDB scheduled jobs that run daily, always on and
not gated behind a feature flag: anonymise_old_audit_events nulls the network
fields, purge_old_audit_events deletes the rows. A deleted row is gone from
every read surface and export.