Skip to main content

Audit Log

View the audit log for system activity tracking.

Overview​

The audit log records privacy-critical and permission-critical actions written to audit_events. Every row has an explicit scope (user, account, or system) and an event-type string.

Rows store entity ids, not names. Each event additionally carries a resolved_names list that resolves the UUIDs it references - the actor plus an allowlist of metadata ids - to display names at read time, so the admin view can show a name alongside every UUID (a deleted/unknown entity keeps only its raw UUID). See Read-time UUID → name resolution.

The table below highlights operator-relevant events; it is a selection, not the complete event catalog. For every recorded type and its scope, see the full event catalog.

typeScopeWritten byTrigger
chat_pii_erasureaccount / systemChat-PII erasure (Chat)Account dissolution (account), a data-subject erasure on one channel's instruction (system, target: "account"), a single-record erasure on one channel's instruction (system, target: "record"), or a Lumio-only erasure (system, target: "lumio")
youtube_member_erasuresystemYouTube member-badge erasure (Member Badges)A member-channel erasure on one channel's instruction
account:role_createdaccountRole management (Roles)A new role is created
account:role_updatedaccountRole managementA role's name/description/color/permissions change
account:role_deletedaccountRole managementA non-system role is deleted
account:member_role_assignedaccountMember managementA member is assigned a different role
account:member_removedaccountMember managementA member is removed from the account
account:invite_created / account:invite_revoked / account:invite_accepted / account:invite_declinedaccountMember management and the recipient's bellAn account invite is created, revoked, accepted, or declined. Bell accept/decline records source: "notification"; invite email and code are never stored
developer:team_invite_accepted / developer:team_invite_declineduserDeveloper-team invite acceptance and the recipient's bellBell acceptance records source: "notification"; decline records invite_deleted and the same source. The invite code and email are never stored
account:dissolvedaccountAccount settings, or the account security workerAn account is irreversibly dissolved; via in metadata is immediate (owner, no grace period) or scheduled (end of the grace period); deleted_history_row_count counts the deleted stream-history rows; deleted_object_count / pending_object_count count its stored files deleted / left queued for retry
account:dissolution_scheduledaccountChannel settings (Danger zone)The owner schedules the account's dissolution; grace_days and scheduled_for in metadata
account:dissolution_cancelledaccountChannel settingsThe owner cancels a scheduled dissolution; scheduled_for (the cancelled date) in metadata
account:identity_changedaccountChannel settingsThe channel's name/picture source, own name or own picture changed; changed (source / name / avatar), source_kind, source_platform in metadata
account:connection_addedaccountChannel connections (Connections)A channel connection is authorized (OAuth callback / exchange)
account:connection_removedaccountChannel connectionsA channel connection is disconnected
account:integration_connected / account:integration_disconnectedaccountShopifyA Shopify store is linked, or disconnected by a member, from the Shopify admin (no actor) or by Shopify's app/uninstalled (no actor)
account:shopify_settings_updatedaccountShopifyA Shopify setting changed, from Lumio (actor = the member) or from the Shopify admin (no actor)
account:shopify_order_name_hiddenaccountShopifyA member hid the buyer name of one Shopify order
account:shopify_data_request / account:shopify_customer_redacted / account:shopify_shop_redactedaccountShopifyA Shopify compliance request was handled: a customer data request was forwarded to the owner, a customer's order events were deleted, or a store's events were deleted (no actor)
account:youtube_revenue_data_deletedaccountYouTube Analytics revenue cleanup (History)Stored revenue rows are deleted; metadata contains platform, reason, rows, never amounts. Available in the admin audit filter.
account:discord_guild_connected / account:discord_guild_disconnectedaccountConnections → DiscordA Discord server is connected through the verified bot invite, or disconnected. A takeover of a server whose bot was removed writes connected for the new account and disconnected (no actor) for the previous one, both with reason: taken_over
account:discord_guild_bot_removedaccountDiscord bot (system)The bot was removed from a connected server on Discord's side; the connection is marked, not deleted
account:discord_guild_reconnectedaccountConnections → DiscordA connected server is invited again; its settings stay
account:obs_config_saved / account:obs_config_deletedaccountConnections → Services (OBS)The account's OBS WebSocket config is saved or removed; mode, remote_host, port, has_password in metadata - never the password
account:extension_kill_switch_setaccountBot modules (Bot modules)The account's extension kill switch is set or cleared (killed in metadata)
account:extension_moderationaccountExtensions (useLumioAction → Chat), bot modulesAn extension moderated chat through the account's bot (source = extension or bot_module, extension_id, install_id, action, platform, target_user_id, duration_secs in metadata; no actor user, never the reason). A bot module's chat:delete is not audited
billing:withdrawal_declared / billing:cancellation_declaredsystem for public, account or user for signed-in withdrawalContract declarationsA withdrawal or public cancellation was received. Public declarations have no user actor.
account:subscription_cancel_requested / extension:subscription_cancel_requestedaccountContract declarationsA signed-in plan or extension subscription cancellation was requested; metadata identifies the declaration without its free-text reason.
billing:withdrawal_processed / billing:cancellation_processedsystemContract declarationsAn operator closed a withdrawal or cancellation as processed or rejected.
billing:declaration_matched / billing:declaration_retention_extendedsystemContract declarationsAn operator matched a public case or raised its retention date.
billing:withdrawal_waiver_acceptedaccountBilling (Billing)A paid checkout completed after the buyer ticked the withdrawal-waiver declaration; kind, text_version, accepted_at and the checkout session in metadata. Proof for § 356 BGB, next to the row in withdrawal_waiver_consents
account:extension_uninstalledaccountExtensions (Extensions)An extension is uninstalled from the account; delete_data_immediately in metadata tells whether its data was deleted at once or kept for 30 days
account:history_revenue_settings_updatedaccountStream history (History)Revenue assumptions changed; metadata lists the changed field names (no values), with no event for an unchanged write
account:history_shared_link_created / account:history_shared_link_revoked / account:history_shared_link_extendedaccountStream history (History)A history report share link (lm_share_*) is created, revoked, or extended (session_id, link_id and effective non-secret options in metadata; never the token string/hash or password)
account:history_session_deletedaccountStream historyA stored stream-history session is deleted (GDPR Art. 17); session_id in metadata
account:webhook_target_blockedaccountAutomations (Webhook call targets)A manual automation run reached a webhook_call node whose target the webhook target filter refused; automation_id, reason and scheme in metadata, never the host, path or query
user:loginuserAuth handlers (REST + GraphQL exchangeToken) and apps/id ingestA web, native, or GraphQL-exchange login succeeds
user:login_faileduserAuth handlers (REST + GraphQL exchangeToken)Provider-token validation or PKCE verification fails
user:oauth_granteduserapps/id ingestA signed-in user grants or reconnects OAuth access
user:api_key_created / user:api_key_updated / user:api_key_revokeduserUser API-key managementA member mints, renames, or revokes a personal lm_usr_* API key; emitted by GraphQL createUserApiKey/updateUserApiKey/deleteUserApiKey and REST POST/PATCH/DELETE /v1/api-keys (only api_key_id + key_prefix in metadata, never the key or its hash)
system:feature_flag_updatedsystemAdmin feature/provider controlsA global feature flag or provider kill-switch changes
system:plan_created / system:plan_updated / system:plan_deletedsystemAdmin plan managementA billing plan is created, edited, or deleted
system:plan_feature_updatedsystemAdmin plan managementA plan↔feature mapping changes
system:account_plan_changedsystemAdmin account controls, or Stripe billing webhookAn admin reassigns an account's plan, or a Stripe subscription event changes it ("via":"stripe" in metadata, actor NULL)
system:account_feature_overridesystemAdmin account controlsAn admin sets or clears an account feature override
system:account_limits_updatedsystemAdmin account controlsA platform operator changes an account's numeric limit overrides; metadata records the target account, write path, and submitted limits, never free-text notes
system:account_deletedsystemAdmin account controlsA platform operator deletes 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 ride in metadata. Owner self-service dissolve is the separate account-scope account:dissolved
system:orphaned_storage_sweptsystemAdmin storage repairA platform operator applies the orphaned extension upload sweep (POST /v1/admin/storage/orphaned-extension-uploads/sweep / adminSweepOrphanedExtensionUploads with apply: true, admin:privacy-erase); "kind":"extension_uploads", orphaned_object_count and queued_object_count ride in metadata. A dry run records nothing
extension:purchase_auto_refundedsystemStripe payment webhook (Webhooks › Automatic refund)A paid extension checkout that cannot be delivered (extension or version deleted or unpublished during checkout: cause: "detached"; account lost access: cause: "not_installable") was refunded in full; actor NULL. purchase_id, cause, amount, currency and stripe_refund_id ride in metadata, never buyer data. A refund Stripe refused records nothing and is only in the API error log
privacy:chatter_objection_added / privacy:chatter_objection_removedsystemChatter objection (Chat › Objection)A platform operator records or lifts a chatter's GDPR Art. 21 objection; platform, "reason":"objection" and already_erased ride in metadata, never the user id or login
privacy:channel_suppression_withdrawnsystemChannel erasure withdrawn (Chat › Erasure)A platform operator withdraws one channel's erasure instruction for one chatter; the channel's later reports name the chatter again. target, account_id, instruction_reference and platform ride in metadata, never the user id or login
system:system_key_created / system:system_key_revokedsystemAdmin credential managementA system API key (is_system=true) is minted or revoked (never full_key/key_hash)
system:connection_upserted / system:connection_deletedsystemAdmin credential managementA system OAuth (login-provider) connection is created/updated or deleted (raw client_secret and ciphertext never stored)
system:user_role_created / system:user_role_updated / system:user_role_deletedsystemAdmin user-role managementA cross-account user role is created, edited, or deleted
system:user_role_assigned / system:user_role_unassignedsystemAdmin user-role managementA cross-account user role is assigned to or removed from a user
system:user_permission_override_set / system:user_permission_override_removedsystemAdmin user-role managementA per-user permission override is set or removed
system:account_permission_override_set / system:account_permission_override_removedsystemAdmin account controlsA platform operator sets or removes an account-level permission override (PUT / DELETE /v1/admin/accounts/{id}/permission-overrides / adminSetAccountPermissionOverride / adminRemoveAccountPermissionOverride); target account_id + permission (+ granted/reason on set) ride in metadata
oauth_client:created / oauth_client:updated / oauth_client:deletedsystemAdmin developer-application managementA developer OAuth client is created, edited, or deleted (oauth-clients:{create,edit,delete}); metadata carries the DB row id (client_id_ref), not the OAuth client_id string, and never the client_secret
admin_role:created / admin_role:updated / admin_role:deletedsystemAdmin admin-role managementAn operator admin role is created, edited, or deleted (admin-roles:{create,edit,delete}) on the admin_roles table (distinct from user roles); role_id / role_name / permissions in metadata
admin_role:assigned / admin_role:unassignedsystemAdmin admin-role managementAn admin role is assigned to or removed from a user (admin-roles:edit); role_id + target_user_id in metadata
system:global_bot_connection_upserted / system:global_bot_connection_deletedsystemAdmin bot-connection managementA global (nil-UUID) bot connection is upserted (manual Discord token or OAuth exchange) or deleted (bot-connections:{create,delete}); platform / connection_id / bot_username / source in metadata, never the token
system:user_deletedsystemAdmin user managementA platform operator deletes a user and all their data (users:delete, DELETE /v1/admin/users/{id} / adminDeleteUser); target_user_id in metadata
system:user_login_connection_deletedsystemAdmin user managementA platform operator removes one of a user's login identities (adminDeleteUserLoginConnection / DELETE /v1/admin/users/{id}/login-connections/{provider}); target_user_id + provider in metadata
system:user_setting_preset_deletedsystemAdmin user managementA platform operator deletes one of a user's setting presets; metadata records the target user, preset id, and kind, never the preset payload
developer:funds_deletion_releasedsystemAdmin developer managementA platform operator releases an unpayable developer balance as a liability and deletes the user or profile (developer-funds:release-deletion); released_balance_id, amount, currency, release_case in metadata
developer:funds_released_claim_startedsystemAdmin developer managementA platform operator records that a person presented the claim code of a released balance; the record is then kept until the claim is settled (developer-funds:settle); released_balance_id, amount, currency in metadata. Once per record
developer:funds_released_settledsystemAdmin developer managementA platform operator credits a released balance to the person's new, verified developer profile (developer-funds:settle); released_balance_id, developer_id, amount, currency in metadata
developer:payout_requesteduserDeveloper payoutsA developer requested a payout, or a closing payout was created for a profile deletion (source); payout id, method, amount, masked destination in metadata
system:developer_deleted / system:developer_team_deletedsystemAdmin developer managementA platform operator deletes a developer or developer team (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_changedsystemAdmin developer managementA platform operator adds/removes a developer-team member or changes their role (developer-limits:edit); team_id, member_id/target_user_id, role_id in metadata
system:developer_revenue_split_setsystemAdmin developer managementA platform operator sets a developer's revenue split (developer-limits:edit); developer_id + split in metadata
system:developer_limits_updatedsystemAdmin developer managementA platform operator updates a developer or extension limit - one merged event discriminated by limit_scope (developer|extension) in metadata (developer-limits:edit)
system:developer_limit_request_reviewedsystemAdmin developer managementA platform operator reviews a developer limit request (developer-limits:edit); request_id, status, developer_id, extension_id, limit_key, has_review_notes in metadata - never the free-text review_notes
system:user_email_changedsystemAdmin user managementA platform operator changes a user's email (users:edit), only on a real change; target_user_id + had_previous_email in metadata - never the address. Distinct from the user-self user:email_changed
system:user_account_creation_override_setsystemAdmin user managementA platform operator sets a user's account-creation override (users:edit); target_user_id + override (allow|deny|default) in metadata
system:user_max_accounts_override_setsystemAdmin user managementA platform operator sets a user's max-accounts override (users:edit) - GraphQL-only; target_user_id + max_accounts in metadata
system:account_connection_deleted / system:account_bot_connection_deletedsystemAdmin account controlsA platform operator deletes an account's channel / bot connection (accounts:edit); target_account_id + platform in metadata. Deleting a Discord server connection (discord-guilds:delete) writes system:account_bot_connection_deleted with platform: discord and guild_id
system:account_login_connection_deletedsystemAdmin account controlsA platform operator deletes an account's login (identity) connection (accounts:edit) - GraphQL-only; target_account_id + provider in metadata
crawler:scope_changedsystemAdmin crawler controlsA platform operator changes the crawl scope
crawler:watchlist_added / crawler:watchlist_removedsystemAdmin crawler controlsA platform operator adds or removes a channel from the crawler watchlist
copyright:moderation_approved / copyright:moderation_dismissedsystemAdmin Copyright ReviewA platform operator (copyright:moderate) approves a report into the global safe or blocked list or dismisses it. Metadata includes spotify_track_id, list, source, and decision, without song title, artist, or reporter identity. Dismissal is recorded only when a report was dismissed
system:account_member_added / system:account_member_removedsystemAdmin account controlsA platform operator adds a user to / removes a member from an account (accounts:edit); target_account_id, target_user_id (+ role_id on add) in metadata; only a real add / removal is recorded
system:coupon_created / system:coupon_updated / system:coupon_deactivatedsystemAdmin couponsA platform operator creates, updates or deactivates a coupon (coupons:{create,edit}); coupon_id + discount_type (create) or changed_fields (update) in metadata - never the coupon code
system:account_se_token_deletedsystemAdmin account controlsA platform operator deletes an account's StreamElements token (se-tokens:delete); target_account_id + token_id in metadata - never the token
system:developer_verification_approved / system:developer_verification_rejectedsystemAdmin developer verificationA platform operator approves or rejects a developer KYC verification (developer-verification:edit); verification_id (+ target_account_id on approve) in metadata - never the rejection reason
system:developer_credit_note_cancelledsystemAdmin developer credit notesA platform operator records a developer objection to a credit note or corrects it (billing:edit); credit_note_id, cancellation_id, cancellation_number, reason, developer_id, reissued_id, reissued_number in metadata - never the supplier's details
system:developer_payout_scheduled / system:developer_payout_sent / system:developer_payout_completed / system:developer_payout_failedsystemDeveloper payoutsThe payout cycle created or sent a developer payout, or it was marked paid / failed by an operator (billing:edit), the cycle or the PayPal webhook (source); payout id, method, amount, status, masked destination in metadata - never the full PayPal e-mail, IBAN or payee details
system:extension_version_approved / system:extension_version_rejectedsystemAdmin extension reviewA platform operator approves or rejects an extension version (extension-review:edit); version_id, extension_id (+ new_status on approve) in metadata - never the notes or reason
system:user_extension_dev_mode_setsystemAdmin developer managementA platform operator toggles a user's extension dev mode (developer-limits:edit); target_user_id + enabled in metadata
system:api_key_updated / system:api_key_rotated / system:api_key_revokedsystemAdmin API KeysA platform operator edits (label + scopes), rotates the secret of, or revokes any account's/user's API key (account-apikeys:{edit,rotate,delete}); 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 mirrored into the target's own log
system:support_code_viewedsystemAdmin user supportA platform operator views a user's support code; metadata contains target_user_id, never the code
system:support_code_checkedsystemAdmin user supportA platform operator checks a user's support code; metadata contains target_user_id and result (match or no_match), never the code
system:user_mfa_resetsystemAdmin user managementA platform operator resets a user's two-factor authentication; metadata contains target_user_id and the required operator reason

The erasure events are hard-delete trails proving after the fact that a subject's data was removed. The account:role_* events capture role and permission changes within an account, and account:connection_* events capture channel connections being added or removed. The user:* rows capture personal security activity, and the system:* rows capture operator actions on global entitlements. See Event types for the exact metadata each emitted event carries.

Scope​

Every audit event carries an explicit scope - one of user, account, or system - stored as a first-class column and set by the writer, never inferred at read time from which of account_id / user_id is populated. Scope partitions the log into three audiences:

ScopeAudienceExample
userThe individual user, self-onlyPersonal-security rows keyed to the authenticated user
accountAn account's members, RBAC-gated to that accountRole/permission change, member-role assignment, channel connection
systemPlatform operatorsInstructed GDPR chat erasures and platform-operator rows

Each scope has its own reader: user → myAuditLog / GET /v1/me/audit-log (self-only, a first-party session or the user's own API key, no permission); account → accountAuditLog / GET /v1/account/audit-log (the account-scope audit-log:read permission); system → the admin log documented on this page (adminAuditLog / GET /v1/admin/audit-log, the admin-scope audit-log:read).

The two erasure types span these scopes: an instructed subject erasure an operator carries out for one channel writes exactly one system-scoped row (account_id = null; the target account is in metadata.account_id), and the account-deletion chat sweep writes a single account-scoped row.

note

The top-level scope column is distinct from the "scope": "account" key that appears inside the account-deletion row's metadata, which describes the breadth of the erasure operation, not the audit audience. The instructed erasure rows carry no such key. The column is authoritative for filtering.

Event types​

chat_pii_erasure​

Written whenever chat PII is hard-deleted for privacy reasons, and for the Lumio-only erasure. Three paths produce it:

  • Account deletion — channel dissolution (immediate or at the end of the grace period) and the operator delete (DELETE /v1/admin/accounts/{id} / GraphQL adminDeleteAccount) — writes one account-scoped event as it purges the account's chat messages, platform events, chatter profiles (platform_users), moderator notes and moderation log. The actor is the owner for a dissolution and the operator for an admin delete:

    { "scope": "account", "account_id": "<uuid>",
    "deleted_message_count": 0, "deleted_event_count": 0,
    "deleted_platform_user_count": 0, "deleted_user_note_count": 0,
    "deleted_moderation_log_count": 0, "deleted_automod_message_count": 0 }
  • Instructed data-subject erasure (POST /v1/admin/privacy/chat/erase / GraphQL eraseChatSubjectData, gated by admin:privacy-erase) acts in the one instructing account only (channel data belongs to the channel — founder ruling ZAF-2626), and writes exactly one system-scoped event (account_id = null, the operator as actor) per request:

    // top-level scope = system
    { "target": "account",
    "account_id": "<uuid>", "case_id": "<uuid>",
    "instruction_reference": "TICKET-123",
    "reason": "Channel instruction by e-mail",
    "initiated_by": "channel", // or "subject"
    "subject_kind": "platform_user", "platform": "twitch", // lumio_user: platform null
    "deleted_message_count": 0,
    "deleted_platform_user_count": 0, "deleted_user_note_count": 0,
    "deleted_automod_message_count": 0,
    "deleted_automod_moderation_log_count": 0,
    "deleted_moderation_log_count": 0,
    "scrubbed_history_session_count": 0, "deleted_account_bot_count": 0,
    "removed_held_queue_entry_count": 0,
    "suppressed": true }

    account_id is the instructing (target) account, instruction_reference the ticket or e-mail id and reason the reason the operator supplied (both required). initiated_by records who initiated the erasure: channel (the channel itself; it may withdraw it later) or subject (the person asked for it; never withdrawable). case_id is Lumio's id of this erasure; its first 8 characters are the case number in the channel owner's confirmation e-mail (below), so support finds the event from the number the owner quotes. The subject itself is never recorded: no platform user id, Lumio user id, login, name or message text — subject_kind (platform_user / lumio_user) and platform are all that identifies the request mode. The two profile counts are the subject's chatter profile (platform_users) and the moderator notes about them in that account. deleted_automod_message_count and deleted_automod_moderation_log_count are the subject's AutoMod history log and its automod_allow / automod_deny moderation-log rows; deleted_moderation_log_count is every other moderation-log row that targeted the subject (bans, timeouts, unbans, deletes, treatment changes). The two moderation-log counts never include the same row, and rows in which the subject was the acting moderator are kept. scrubbed_history_session_count is the number of the account's stream-history sessions whose frozen top chatters, links, commands, keywords, announcements or pins named the subject and were rewritten; deleted_account_bot_count is the subject's bot-list entry removed. removed_held_queue_entry_count is the subject's still-open messages closed in the channel's live AutoMod held queue (best-effort, 0 when Redis is unreachable). suppressed: true records that the subject was added to the account's suppression list (account_chatter_suppressions) in the same operation (no separate event). No per-account rows are written, and the event lists no other accounts.

    After the deletes Lumio e-mails the channel's owner a confirmation (ZAF-2640 F9): channel, date, what was deleted (the kinds with a count above 0) and the case number. It never names the person, their platform or the instruction_reference, which may identify them. No mail is sent when nothing was deleted, when the channel has no owner or the owner no e-mail address. The mail is best-effort: a failure is logged and never fails the erasure. It is a required notice with no in-app entry and no opt-out.

  • Lumio-only erasure (POST /v1/admin/privacy/lumio/erase / GraphQL eraseChatSubjectAtLumio, gated by admin:privacy-erase) touches no channel's data: it writes the person's erasure entry in the global Lumio list (chatter_suppressions, acting on the Lumio Stats leaderboard) and, on YouTube, clears Lumio's member cache across every account. One system-scoped event (account_id = null) per request, again without the person's id or login:

    // top-level scope = system
    { "target": "lumio", "instruction_reference": "TICKET-123",
    "reason": "Art. 17 request",
    "subject_kind": "platform_user", "platform": "youtube",
    "suppressed": true, "erased_key_count": 0 }
  • Record erasure (POST /v1/admin/privacy/records/erase / GraphQL eraseChatRecord, gated by admin:privacy-erase) erases one record in one channel on that channel's instruction: one chat message, moderator note, moderation-log entry, AutoMod-log entry, bot-list entry, or one person's entries in one stream report. One system-scoped event per erased record, with the record's internal id, the instruction reference and the operator's reason, never a name, message text or platform user id (platform only for a stream report). A record that is not found writes no event:

    // top-level scope = system
    { "target": "record", "record_kind": "chat_message",
    "record_id": "6f0c…", "account_id": "5c1e…", "case_id": "3f9c…",
    "instruction_reference": "TICKET-123", "reason": "Channel instruction",
    "platform": null, "deleted_record_count": 1,
    "moderation_log_text_removed_count": 1, "deleted_moderation_log_count": 0,
    "held_queue_entry_removed": false, "scrubbed_entry_count": 0 }

privacy:chatter_objection_added / privacy:chatter_objection_removed​

Written by the chatter objection path (GDPR Art. 21: POST /v1/admin/privacy/chat/objections / GraphQL addChatterObjection, gated by admin:privacy-objection-create; DELETE /v1/admin/privacy/chat/objections/{platform}/{user_id} / GraphQL removeChatterObjection, gated by admin:privacy-objection-delete). One system-scoped row (account_id = null, the operator as actor) per change:

{ "platform": "twitch", "reason": "objection", "already_erased": false }

The chatter's user id and login are deliberately left out. already_erased is true when the chatter's entry in the global Lumio list (chatter_suppressions) is already an erasure entry. An objection acts on Lumio Stats only and deletes nothing. A lift that fails (nothing recorded, or an erasure entry) records nothing.

privacy:channel_suppression_withdrawn​

Written when a channel withdraws its erasure instruction for one chatter (POST /v1/admin/privacy/chat/erase/withdraw / GraphQL withdrawChannelChatterSuppression, gated by admin:privacy-erase-withdraw). The chatter's entry in that account's account_chatter_suppressions is deleted, so the channel's reports frozen afterwards name them again; nothing the erasure deleted comes back. One system-scoped row (account_id = null, the operator as actor):

{ "target": "account", "account_id": "…", "instruction_reference": "T-123",
"platform": "twitch" }

The chatter's user id and login are left out. Only an erasure the channel initiated can be withdrawn. A withdrawal that fails (unknown account, no entry for the chatter, or a 409 because the person asked for the erasure) records nothing.

youtube_member_erasure​

Written by the YouTube member-channel erasure (DELETE /v1/admin/privacy/youtube/member/{member_channel_id}?account_id=…&instruction_reference=…&initiated_by=… / GraphQL eraseYoutubeMemberData, gated by admin:privacy-erase). It clears Lumio's own cached member records in Redis across every account (Lumio's YouTube API client cache, not channel data) and erases the member's channel data in the instructing account only, exactly as the chat_pii_erasure subject erasure above. It writes exactly one system-scoped event (account_id = null) per request, with the same metadata as the chat_pii_erasure subject event (subject_kind: "platform_user", platform: "youtube") plus erased_key_count:

// top-level scope = system
{ "target": "account",
"account_id": "<uuid>", "case_id": "<uuid>",
"instruction_reference": "TICKET-123", "initiated_by": "channel",
"subject_kind": "platform_user", "platform": "youtube",
"erased_key_count": 0, "deleted_message_count": 0,
"deleted_platform_user_count": 0, "deleted_user_note_count": 0,
"deleted_automod_message_count": 0,
"deleted_automod_moderation_log_count": 0,
"deleted_moderation_log_count": 0,
"scrubbed_history_session_count": 0, "deleted_account_bot_count": 0,
"removed_held_queue_entry_count": 0,
"suppressed": true }

The member channel id is never recorded. The channel owner gets the same confirmation e-mail as for chat_pii_erasure; it lists the channel membership only when the instructing channel itself had the member cached.

Both event types record the acting admin in user_id and their request IP in ip_address (GeoIP-enriched into country / city - see Location data). Their origin is request-derived (the admin REST route or its GraphQL twin), so they also record the user_agent.

account:role_* (role & permission changes)​

Emitted whenever an account's roles or member-role assignments change, via both GraphQL (createRole / updateRole / deleteRole / updateMemberRole) and their REST counterparts (POST /v1/roles, PATCH /v1/roles/{id}, DELETE /v1/roles/{id}, PATCH /v1/accounts/{id}/members/{membership_id}/role). Both protocols write identical rows. Every event is account-scoped, carries the acting user_id (the actor), the tenant account_id, and the request ip_address (GeoIP-enriched). These rows surface only in the account log - never the actor's personal (user-scoped) log (overlap rule).

  • account:role_created / account:role_updated / account:role_deleted:

    { "role_id": "<uuid>", "role_name": "Moderators", "slug": "moderators" }
  • account:member_role_assigned:

    { "membership_id": "<uuid>", "target_user_id": "<uuid>",
    "role_id": "<uuid>", "role_name": "Moderators",
    "personal_keys_affected": 2, "popout_tokens_affected": 1 }

    The two counts are the member's live personal API keys and popout tokens. Their scopes follow the new role from the next request; a count is null when it could not be read.

The audit insert is best-effort: a failed write is logged and swallowed, so the role mutation itself never fails on the audit side-channel.

account:connection_* (channel connections)​

Emitted whenever a channel connection is added or removed. Add (account:connection_added) happens at OAuth completion, which is REST-only: the session-bound JSON exchanges (POST /connections/channel/{platform}/exchange and the unified POST /connections/oauth/{platform}/exchange). There is no GraphQL "add connection" mutation to mirror, because authorization is an HTTP redirect flow. Remove (account:connection_removed) has full parity: GraphQL disconnectChannel and REST DELETE /v1/connections/channel/{platform} write identical rows. Every event is account-scoped, carries the tenant account_id, the request ip_address (GeoIP-enriched), and the platform:

{ "platform": "twitch" }

The acting user_id is recorded on the authenticated paths (exchange, disconnect). The unauthenticated browser-redirect callback carries no caller identity, so its actor is null. Like the role events, these rows surface only in the account log (overlap rule) and the insert is best-effort - a failed write never fails the connection mutation.

For YouTube, account:connection_added also records optional_scopes_granted as the list of optional scope names granted by Google; [] means none were granted. It contains neither a token nor the complete grant.

account:youtube_revenue_data_deleted​

This account-scoped event records deletion of stored YouTube Analytics revenue rows. It appears only when at least one row was removed. The actor is the user who triggered deletion, or absent for a worker cleanup. The metadata contains no revenue amount:

{ "platform": "youtube", "reason": "scope_removed", "rows": 12 }

reason is disconnected, scope_removed, revoked, auth_unconfirmed_30d, video_deleted, account_deleted, or report_deleted. The admin viewer includes this event type in its label and type filter.

user:* (personal security events)​

User-scope rows are personal security events. They always use account_id = null; when the event can be tied to a known Lumio user, user_id is the subject who can see the row in My Activity. If a failed login cannot be mapped to a known Lumio user, the row is still scope=user but has user_id = null, so it is visible only in the admin audit log.

  • user:login - successful login. apps/id emits the web login path through POST /v1/internal/audit-ingest; the Rust POST /v1/auth/token/exchange handler emits the native PKCE redemption path directly; the GraphQL exchangeToken mutation emits its own success inline (method: graphql_exchange) - it has no first-party caller, so apps/id ingest never covers it.

    { "method": "graphql_exchange", "provider": "twitch", "is_new_user": false }
    { "method": "pkce" }
  • user:mfa_enabled / user:mfa_disabled - the first or last 2FA method; method identifies an authenticator app (totp) or passkey. user:mfa_factor_added / user:mfa_factor_removed record a further method being added or removed; metadata includes method type, factor ID and resulting count, never the user-chosen label.

  • user:login_failed - rejected login emitted by the Rust REST /auth handlers and the GraphQL exchangeToken mutation (at parity) when provider-token validation fails or PKCE verification fails.

    { "provider": "twitch", "reason": "provider_token_invalid" }
    { "reason": "pkce_verification_failed" }
  • user:oauth_granted - provider link/reconnect consent from a signed-in user, emitted by apps/id through the ingest endpoint.

    { "provider": "youtube", "reconnect": true, "connection_id": "<uuid>" }
  • user:api_key_created / user:api_key_updated / user:api_key_revoked - a member mints, renames, or revokes a personal lm_usr_* API key, via GraphQL createUserApiKey / updateUserApiKey / deleteUserApiKey and REST POST / PATCH / DELETE /v1/api-keys. Metadata carries only the non-secret api_key_id + key_prefix; never the key or its hash.

    { "api_key_id": "<uuid>", "key_prefix": "lm_usr_ab12" }

apps/id forwards the end-user IP and user-agent to the ingest endpoint; the Rust auth handlers capture them from the request. Both paths GeoIP-enrich the IP fail-open.

system:* (platform-operator actions)​

Emitted for platform-operator actions on the global entitlement surface, reachable from the admin app. Every event is system-scoped, carries the acting admin as user_id and the request ip_address (GeoIP-enriched), and - unlike the account events - carries no tenant account_id column. A target account (for the per-account overrides) is recorded in metadata instead, so a system row can never surface in that account's own audit log; it appears only on this admin system surface. GraphQL and REST write identical rows, and the insert is best-effort (a failed write never fails the admin mutation).

  • system:feature_flag_updated - a global feature-flag toggle. Emitted by GraphQL adminUpdateFeatureFlag / adminUpdateFeatureFlagDetail, REST PATCH /v1/admin/feature-flags/{id} / PUT /v1/admin/feature-flags/{id}/detail, and the platform provider kill-switches - GraphQL adminSetProviderEnabled / adminSetProviderSubtypeEnabled / adminToggleProvider / adminToggleProviderSubtype and REST PATCH /v1/admin/providers/{platform}[/{subtype}]/toggle and …/enabled, which flip the same platform:* flags. The provider paths add "via": "provider" plus the platform (and subtype) to the metadata; enabled is always the resulting flag state.

    { "flag_id": "<uuid>", "flag_key": "platform:twitch", "enabled": false,
    "via": "provider", "platform": "twitch" }
  • system:plan_created / system:plan_updated / system:plan_deleted - plan lifecycle, via GraphQL adminCreatePlan / adminUpdatePlan / adminDeletePlan and REST POST /v1/admin/plans, PATCH /v1/admin/plans/{id}, DELETE /v1/admin/plans/{id}.

    { "plan_id": "<uuid>", "slug": "pro", "name": "Pro" }
  • system:plan_feature_updated - a plan↔feature mapping change, via GraphQL adminSetPlanFeature and REST PUT /v1/admin/plans/{id}/features.

    { "plan_id": "<uuid>", "feature_id": "<uuid>", "enabled": true }
  • system:account_plan_changed - an admin reassigns an account's plan, via GraphQL adminUpdateAccountPlan and REST PATCH /v1/admin/accounts/{id}/plan.

    { "account_id": "<uuid>", "plan_id": "<uuid>" }
  • system:account_feature_override - an admin sets or clears a per-account feature override, via GraphQL adminSetAccountFeature / adminClearAccountFeature and REST PUT /v1/admin/accounts/{id}/features / DELETE /v1/admin/accounts/{id}/features/{feature_id}. The action field is set or clear.

    { "account_id": "<uuid>", "feature_id": "<uuid>", "action": "set", "enabled": true }
  • system:orphaned_storage_swept - a platform operator applies the orphaned extension upload sweep (GraphQL adminSweepOrphanedExtensionUploads / REST POST /v1/admin/storage/orphaned-extension-uploads/sweep, apply: true only). Metadata carries kind, orphaned_object_count and queued_object_count.

  • extension:purchase_auto_refunded - the Stripe checkout.session.completed webhook refunded an undeliverable paid extension checkout. No actor. Metadata carries purchase_id, cause (detached | not_installable), amount, currency and stripe_refund_id.

  • system:account_deleted - a platform operator deletes an account, via GraphQL adminDeleteAccount and REST DELETE /v1/admin/accounts/{id}. Emitted only on a real delete; the deleted account id is the target (rides in metadata, not the key column). Audit rows live in TimescaleDB, which the account CASCADE does not erase, so the record persists. Owner self-service dissolve is the separate account-scope account:dissolved.

    { "account_id": "<uuid>", "reason": "operator_delete" }
  • system:account_permission_override_set / system:account_permission_override_removed - a platform operator (accounts:overrides-edit) grants/revokes an account-level permission override, via GraphQL adminSetAccountPermissionOverride / adminRemoveAccountPermissionOverride and REST PUT /v1/admin/accounts/{id}/permission-overrides / DELETE /v1/admin/accounts/{id}/permission-overrides/{permission}. System-scoped because it is a cross-account operator action on a tenant's granted-permission surface; the target account is the metadata account_id (never the key column). Remove emits only when a row was actually removed. The set path additionally records granted and the operator's optional reason; metadata never carries a secret.

    { "account_id": "<uuid>", "permission": "chat:ban", "granted": true, "reason": "temporary escalation" }

Viewing Logs​

Access the audit log via Admin > Audit Log (/audit-log). A filter card at the top exposes:

  • Type - a dropdown with an "all types" entry and the catalogued audit event types, including erasure, crawler, account, user, and system events.
  • User ID - free-text.
  • Account ID - free-text.
  • From / To - date pickers.

Filters apply on Apply; changing a filter alone does not refetch until the button is pressed (or the page changes).

Results render as a table with Time, Type, User ID (truncated to the first 8 characters), IP, Device, Location (city, country), and a Details toggle column. The Device column parses the event's captured user agent at render time into a compact device / browser / operating-system summary with a device icon (e.g. "Chrome 141 · macOS"). It never shows a raw or bogus value and never guesses: a user agent it cannot read - absent, a non-browser client such as node/undici/axios, a legacy web-server string, or junk - renders as the localized Unknown ("Unbekannt" in German), as does a missing IP; partial hits (a known browser with an unreadable OS, or vice-versa) still show the part that is known. Clicking a row expands it to show the raw event metadata JSON and, when present, the full raw user agent string - the forensic source of truth the Device column is derived from. The current table does not show the top-level scope column; use the API-level scope filter when you need to narrow the admin view by audience.

Paging is 50 events per page with Previous / Next controls and a "page X of Y (N events)" counter.

Location data​

The Location column (and the country / city fields on each event) is filled by the optional GeoIP service, which resolves the event's client IP to a coarse country/city via MaxMind GeoLite2. country holds the ISO-3166-1 alpha-2 code (e.g. DE) and city the English city name; the column renders them together as city, country.

Enrichment is fail-open: country / city are left empty whenever GeoIP is disabled, its database is unavailable, or the client IP is missing, private/loopback, on the MaxMind privacy-exclusion list, or simply not found. The IP address itself is still recorded in those cases, and the audit log is fully functional either way.

GeoIP is disabled by default, so out of the box these fields are empty and the column shows nothing. Enabling GeoIP requires a MaxMind license key and, optionally, honours user opt-outs through the Privacy Exclusions API; see the GeoIP configuration for the LUMIO__GEOIP__* settings.

Permissions​

Reading the audit log requires the audit-log:read permission. The dashboard entry itself is gated on admin:access. The seeded system_admin role carries both permissions explicitly; is_system: true protects that role from edits and deletion, but does not bypass permission checks. The erasure operations are separately gated by admin:privacy-erase (see Roles & Permissions).

This admin page is the operator audit surface and uses the admin-scope audit-log:read (checked against the caller's admin role). The account- and user-scoped logs are separate surfaces: accountAuditLog / GET /v1/account/audit-log is gated by the identically-named account-scope audit-log:read (a distinct permission checked against the account role), and myAuditLog / GET /v1/me/audit-log is self-only, gated purely by a first-party session with no permission. An account grant never unlocks this admin log, and vice-versa.

Storage​

Audit events are persisted in TimescaleDB and queried via the admin API (GET /v1/admin/audit-log, GraphQL adminAuditLog). The admin UI reaches it through its own Next.js proxy route at /api/audit-log.

Each event exposes its scope on both protocols. The admin query accepts an optional scope filter - GET /v1/admin/audit-log?scope=system or the scope field on the adminAuditLog filter input - to narrow results to a single audience (user | account | system); an unrecognized value is ignored. The admin log is a superuser view, so with no scope filter it returns rows from every scope. The admin Next.js proxy and UI do not currently forward a scope filter.