Skip to main content

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.

ScopeAudienceExample events
userThe individual user, self-onlyPersonal-security rows keyed to the authenticated user
accountAn account's members, gated to that accountRole/permission changes, member-role assignment, channel connections
systemPlatform operatorsInstructed 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 paramGraphQL filter fieldMeaning
pagepagePage number (default 1).
limitlimitPage size (REST default 50, clamped 1..=200).
typeeventTypeExact event-type match.
fromdateFromOnly events at/after this instant (RFC-3339; an unparseable value is ignored).
todateToOnly 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 userAgent is 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. the from/to accounts 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 typeScopeCurrent writer
chat_pii_erasureaccount / systemaccount: 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_erasuresystemAdmin YouTube member-channel erasure on one channel's instruction
account:role_createdaccountRole creation
account:role_updatedaccountRole edits, including permission changes
account:role_deletedaccountCustom role deletion
account:member_role_assignedaccountMember 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_removedaccountA member was removed from the account (target user in metadata)
account:member_leftaccountA member left their own account; actor is that member. Metadata: membership_id
account:invite_createdaccountAn account invite was created (invite email and code are never stored in metadata)
account:invite_revokedaccountAn account invite was revoked/deleted
account:invite_acceptedaccountAn 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_declinedaccountThe 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_requestedaccountAn account member requested cancellation of a subscription. Metadata: plan, period_end, cancellation kind, and has_reason; the reason itself is not stored
account:dissolvedaccountAn 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_scheduledaccountThe owner scheduled the account's dissolution (scheduleAccountDissolution / POST /v1/accounts/{id}/dissolution); actor is the owner. Metadata: grace_days, scheduled_for
account:dissolution_cancelledaccountThe owner cancelled a scheduled dissolution (cancelAccountDissolution / DELETE /v1/accounts/{id}/dissolution); actor is the owner. Metadata: scheduled_for (the cancelled date)
account:identity_changedaccountThe 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_addedaccountChannel 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_removedaccountChannel disconnect
account:integration_connectedaccountA 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_disconnectedaccountA 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_updatedaccountA 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_hiddenaccountA member hid the buyer name of one Shopify order (both of its events). Metadata platform, event_id; never the name
account:shopify_data_requestaccountShopify 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_redactedaccountThe listed orders' events were deleted (customers/redact, no actor), after the second pass. Metadata platform, shop_host, orders (count)
account:shopify_shop_redactedaccountThe 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_deletedaccountStored 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_connectedaccountA 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_disconnectedaccountA 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_removedaccountThe 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_reconnectedaccountA 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_setaccountThe extension kill switch was set or cleared (POST /v1/bot-modules/kill / setBotModuleKillSwitch, bot-modules:edit). Metadata: killed only
account:extension_moderationaccountAn 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_uninstalledaccountAn 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_requiredaccountA 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_upsertedaccountPer-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_deletedaccountPer-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_blockedaccountA 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_outaccountBroadcaster hides a channel from the public Stats app (is_public → false)
public_stats:channel_opted_inaccountBroadcaster re-lists a channel on the public Stats app (is_public → true)
billing:withdrawal_declared / billing:cancellation_declaredsystem for public; account or user for signed-in withdrawalA 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_requestedaccountA 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_processedsystemAn 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_extendedsystemAn operator matched a public case to a contract or extended its retention date.
billing:withdrawal_waiver_acceptedaccountA 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_deletedaccountPopout-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_revokedaccountOverlay 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_extendedaccountShared-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_revokedaccountWidget 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_revokedaccountPer-overlay access grant (upsert - resulting role in metadata) / revoke; target_user_id in metadata
account:widget_access_granted / account:widget_access_revokedaccountPer-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_extendedaccountStream-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_updatedaccountA 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_updatedaccountA 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_updatedaccountRevenue 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_deletedaccountA 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_unsharedaccountA 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_assignedaccountAn 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_removedaccountAn 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_revokedaccountAn 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_acceptedaccountExtension-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_deletedaccountThe 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_deletedaccountA 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_rotatedaccountAn 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_rotateduserA 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_revokeduserA 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:loginuserSuccessful 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_faileduserRejected 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_granteduserProvider link/reconnect consent by an authenticated user (emitted by apps/id via audit-ingest)
user:login_reconnect_requireduserA login-connection token refresh failed terminally; the Token Refresh Worker auto-flagged reconnect_required
user:active_account_switcheduserThe 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:logoutuserThe 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_revokeduserThe 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_requesteduserA user requested or resent an email sign-in verification link. Metadata: purpose and resend, never the address or link
user:email_login_addeduserEmail sign-in was enabled after registration or dashboard setup. Metadata: via (register or dashboard)
user:email_login_changeduserA user completed an email sign-in address change; metadata is empty
user:email_login_cancelleduserA user cancelled pending email sign-in setup or change; metadata is empty
user:email_verifieduserAn email address was verified during registration, sign-in setup or address change. Metadata: purpose (register, add or change), never the address
user:login_method_removeduserA user removed email sign-in. Metadata: method (email)
user:password_changeduserA user changed their password; metadata is empty
user:password_reset_requesteduserA password reset was requested for a resolved user; metadata is empty
user:password_reset_completeduserA user completed a password reset; metadata is empty
user:email_changeduserThe 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_changeduserThe 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_requesteduserA user requested their own data export. Metadata: export_id, export model, account_ids, and include_chat; never export contents
user:data_export_downloadeduserA user downloaded their own export. Metadata: export_id and model; never export contents
user:deletion_scheduleduserA user scheduled deletion of their own profile. Metadata: grace_days and scheduled_for
user:deletion_cancelleduserA user cancelled their scheduled profile deletion; metadata is empty
user:data_export_cancelleduserThe 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_removeduserA 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_submitteduserAn 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_updateduserA 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_requesteduserA 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_setuserA 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_deleteduserA 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_changeduserA 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_deleteduserA 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_deleteduserDeveloper-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_removeduserA 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_accepteduserDeveloper-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_declineduserThe 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_disableduserA 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_removeduserA 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_faileduserA two-factor challenge failed. Metadata: challenge context
user:mfa_recovery_code_useduserA recovery code was used. Metadata: context and codes remaining
user:mfa_recovery_codes_regenerateduserA user regenerated their recovery codes; metadata is empty
user:mfa_trusted_device_revokeduserA user revoked one or all trusted devices. Metadata: device_id, all, and affected count
user:step_up_verifieduserA user confirmed a sensitive action with step-up authentication. Metadata: verification method
crawler:scope_changedsystemA platform operator changes the crawler scope
crawler:watchlist_addedsystemA platform operator adds a channel to the crawler watchlist
crawler:watchlist_removedsystemA platform operator removes a channel from the crawler watchlist
copyright:moderation_approved / copyright:moderation_dismissedsystemA 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_viewedsystemA platform operator revealed a user's support code. Metadata: target_user_id, never the code
system:support_code_checkedsystemA platform operator checked a user's support code. Metadata: target_user_id and result (match or no_match), never the code
system:user_mfa_resetsystemA platform operator reset a user's two-factor authentication. Metadata: target_user_id and required operator reason
system:feature_flag_updatedsystemAdmin global feature-flag toggles and platform provider kill-switches
system:plan_created / system:plan_updated / system:plan_deletedsystemAdmin plan lifecycle (create / edit / delete)
system:plan_feature_updatedsystemAdmin plan↔feature mapping changes
system:account_plan_changedsystemAn 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_overridesystemAdmin sets or clears a per-account feature override
system:account_limits_updatedsystemA 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_deletedsystemA 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_deletedsystemA 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_sweptsystemA 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_refundedsystemThe 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_addedsystemA 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_removedsystemA 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_withdrawnsystemA 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_createdsystemAdmin mints a system API key (is_system=true); the permission set is registry-validated and a bounded expires_at is enforced
system:system_key_revokedsystemAdmin revokes a system API key (only the key id in metadata; never full_key/key_hash)
system:connection_upsertedsystemAdmin creates/updates a system OAuth (login-provider) connection; the raw client_secret and its AES-256-GCM ciphertext are never stored
system:connection_deletedsystemAdmin deletes a system OAuth connection
system:user_role_created / system:user_role_updated / system:user_role_deletedsystemAdmin cross-account user-role lifecycle (create / edit / delete)
system:user_role_assigned / system:user_role_unassignedsystemAdmin assigns/removes a cross-account user role for a user (target user in metadata)
system:user_permission_override_set / system:user_permission_override_removedsystemAdmin sets/removes a per-user permission override (target user in metadata)
system:account_permission_override_set / system:account_permission_override_removedsystemA 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:deletedsystemOperator 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:deletedsystemOperator 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:unassignedsystemOperator 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_deletedsystemGlobal (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_deletedsystemA 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_deletedsystemA 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_releasedsystemA 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_startedsystemA 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_settledsystemA 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_deletedsystemA 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_changedsystemOperator 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_setsystemA 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_updatedsystemA 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_reviewedsystemA 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_changedsystemA 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_setsystemA 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_setsystemA 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_deletedsystemA 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_deletedsystemA 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_removedsystemA 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_deactivatedsystemA 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_deletedsystemA 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_rejectedsystemA 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_cancelledsystemA 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_failedsystemThe 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_rejectedsystemA 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_setsystemA 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_revokedsystemA 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
apps/id-emitted rows require the ingest key

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:

AgeWhat is kept
0–12 monthsthe full row
12–24 monthsthe 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 monthsnothing - 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.