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.
type | Scope | Written by | Trigger |
|---|---|---|---|
chat_pii_erasure | account / system | Chat-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_erasure | system | YouTube member-badge erasure (Member Badges) | A member-channel erasure on one channel's instruction |
account:role_created | account | Role management (Roles) | A new role is created |
account:role_updated | account | Role management | A role's name/description/color/permissions change |
account:role_deleted | account | Role management | A non-system role is deleted |
account:member_role_assigned | account | Member management | A member is assigned a different role |
account:member_removed | account | Member management | A member is removed from the account |
account:invite_created / account:invite_revoked / account:invite_accepted / account:invite_declined | account | Member management and the recipient's bell | An 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_declined | user | Developer-team invite acceptance and the recipient's bell | Bell acceptance records source: "notification"; decline records invite_deleted and the same source. The invite code and email are never stored |
account:dissolved | account | Account settings, or the account security worker | An 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_scheduled | account | Channel settings (Danger zone) | The owner schedules the account's dissolution; grace_days and scheduled_for in metadata |
account:dissolution_cancelled | account | Channel settings | The owner cancels a scheduled dissolution; scheduled_for (the cancelled date) in metadata |
account:identity_changed | account | Channel settings | The channel's name/picture source, own name or own picture changed; changed (source / name / avatar), source_kind, source_platform in metadata |
account:connection_added | account | Channel connections (Connections) | A channel connection is authorized (OAuth callback / exchange) |
account:connection_removed | account | Channel connections | A channel connection is disconnected |
account:integration_connected / account:integration_disconnected | account | Shopify | A 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_updated | account | Shopify | A Shopify setting changed, from Lumio (actor = the member) or from the Shopify admin (no actor) |
account:shopify_order_name_hidden | account | Shopify | A member hid the buyer name of one Shopify order |
account:shopify_data_request / account:shopify_customer_redacted / account:shopify_shop_redacted | account | Shopify | A 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_deleted | account | YouTube 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_disconnected | account | Connections → Discord | A 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_removed | account | Discord bot (system) | The bot was removed from a connected server on Discord's side; the connection is marked, not deleted |
account:discord_guild_reconnected | account | Connections → Discord | A connected server is invited again; its settings stay |
account:obs_config_saved / account:obs_config_deleted | account | Connections → 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_set | account | Bot modules (Bot modules) | The account's extension kill switch is set or cleared (killed in metadata) |
account:extension_moderation | account | Extensions (useLumioAction → Chat), bot modules | An 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_declared | system for public, account or user for signed-in withdrawal | Contract declarations | A withdrawal or public cancellation was received. Public declarations have no user actor. |
account:subscription_cancel_requested / extension:subscription_cancel_requested | account | Contract declarations | A signed-in plan or extension subscription cancellation was requested; metadata identifies the declaration without its free-text reason. |
billing:withdrawal_processed / billing:cancellation_processed | system | Contract declarations | An operator closed a withdrawal or cancellation as processed or rejected. |
billing:declaration_matched / billing:declaration_retention_extended | system | Contract declarations | An operator matched a public case or raised its retention date. |
billing:withdrawal_waiver_accepted | account | Billing (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_uninstalled | account | Extensions (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_updated | account | Stream 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_extended | account | Stream 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_deleted | account | Stream history | A stored stream-history session is deleted (GDPR Art. 17); session_id in metadata |
account:webhook_target_blocked | account | Automations (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:login | user | Auth handlers (REST + GraphQL exchangeToken) and apps/id ingest | A web, native, or GraphQL-exchange login succeeds |
user:login_failed | user | Auth handlers (REST + GraphQL exchangeToken) | Provider-token validation or PKCE verification fails |
user:oauth_granted | user | apps/id ingest | A signed-in user grants or reconnects OAuth access |
user:api_key_created / user:api_key_updated / user:api_key_revoked | user | User API-key management | A 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_updated | system | Admin feature/provider controls | A global feature flag or provider kill-switch changes |
system:plan_created / system:plan_updated / system:plan_deleted | system | Admin plan management | A billing plan is created, edited, or deleted |
system:plan_feature_updated | system | Admin plan management | A plan↔feature mapping changes |
system:account_plan_changed | system | Admin account controls, or Stripe billing webhook | An admin reassigns an account's plan, or a Stripe subscription event changes it ("via":"stripe" in metadata, actor NULL) |
system:account_feature_override | system | Admin account controls | An admin sets or clears an account feature override |
system:account_limits_updated | system | Admin account controls | A 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_deleted | system | Admin account controls | A 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_swept | system | Admin storage repair | A 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_refunded | system | Stripe 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_removed | system | Chatter 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_withdrawn | system | Channel 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_revoked | system | Admin credential management | A system API key (is_system=true) is minted or revoked (never full_key/key_hash) |
system:connection_upserted / system:connection_deleted | system | Admin credential management | A 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_deleted | system | Admin user-role management | A cross-account user role is created, edited, or deleted |
system:user_role_assigned / system:user_role_unassigned | system | Admin user-role management | A cross-account user role is assigned to or removed from a user |
system:user_permission_override_set / system:user_permission_override_removed | system | Admin user-role management | A per-user permission override is set or removed |
system:account_permission_override_set / system:account_permission_override_removed | system | Admin account controls | A 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:deleted | system | Admin developer-application management | A 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:deleted | system | Admin admin-role management | An 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:unassigned | system | Admin admin-role management | An 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_deleted | system | Admin bot-connection management | A 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_deleted | system | Admin user management | A 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_deleted | system | Admin user management | A 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_deleted | system | Admin user management | A 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_released | system | Admin developer management | A 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_started | system | Admin developer management | A 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_settled | system | Admin developer management | A 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_requested | user | Developer payouts | A 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_deleted | system | Admin developer management | A 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_changed | system | Admin developer management | A 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_set | system | Admin developer management | A platform operator sets a developer's revenue split (developer-limits:edit); developer_id + split in metadata |
system:developer_limits_updated | system | Admin developer management | A 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_reviewed | system | Admin developer management | A 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_changed | system | Admin user management | A 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_set | system | Admin user management | A 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_set | system | Admin user management | A 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_deleted | system | Admin account controls | A 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_deleted | system | Admin account controls | A platform operator deletes an account's login (identity) connection (accounts:edit) - GraphQL-only; target_account_id + provider in metadata |
crawler:scope_changed | system | Admin crawler controls | A platform operator changes the crawl scope |
crawler:watchlist_added / crawler:watchlist_removed | system | Admin crawler controls | A platform operator adds or removes a channel from the crawler watchlist |
copyright:moderation_approved / copyright:moderation_dismissed | system | Admin Copyright Review | A 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_removed | system | Admin account controls | A 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_deactivated | system | Admin coupons | A 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_deleted | system | Admin account controls | A 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_rejected | system | Admin developer verification | A 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_cancelled | system | Admin developer credit notes | A 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_failed | system | Developer payouts | The 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_rejected | system | Admin extension review | A 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_set | system | Admin developer management | A 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_revoked | system | Admin API Keys | A 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_viewed | system | Admin user support | A platform operator views a user's support code; metadata contains target_user_id, never the code |
system:support_code_checked | system | Admin user support | A 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_reset | system | Admin user management | A 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:
| Scope | Audience | Example |
|---|---|---|
user | The individual user, self-only | Personal-security rows keyed to the authenticated user |
account | An account's members, RBAC-gated to that account | Role/permission change, member-role assignment, channel connection |
system | Platform operators | Instructed 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.
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}/ GraphQLadminDeleteAccount) — 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/ GraphQLeraseChatSubjectData, gated byadmin: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_idis the instructing (target) account,instruction_referencethe ticket or e-mail id andreasonthe reason the operator supplied (both required).initiated_byrecords who initiated the erasure:channel(the channel itself; it may withdraw it later) orsubject(the person asked for it; never withdrawable).case_idis 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) andplatformare 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_countanddeleted_automod_moderation_log_countare the subject's AutoMod history log and itsautomod_allow/automod_denymoderation-log rows;deleted_moderation_log_countis 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_countis 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_countis the subject's bot-list entry removed.removed_held_queue_entry_countis the subject's still-open messages closed in the channel's live AutoMod held queue (best-effort, 0 when Redis is unreachable).suppressed: truerecords 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/ GraphQLeraseChatSubjectAtLumio, gated byadmin:privacy-erase) touches no channel's data: it writes the person'serasureentry 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/ GraphQLeraseChatRecord, gated byadmin: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 (platformonly 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
nullwhen 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 throughPOST /v1/internal/audit-ingest; the RustPOST /v1/auth/token/exchangehandler emits the native PKCE redemption path directly; the GraphQLexchangeTokenmutation 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;methodidentifies an authenticator app (totp) or passkey.user:mfa_factor_added/user:mfa_factor_removedrecord 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/authhandlers and the GraphQLexchangeTokenmutation (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 personallm_usr_*API key, via GraphQLcreateUserApiKey/updateUserApiKey/deleteUserApiKeyand RESTPOST/PATCH/DELETE /v1/api-keys. Metadata carries only the non-secretapi_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 GraphQLadminUpdateFeatureFlag/adminUpdateFeatureFlagDetail, RESTPATCH /v1/admin/feature-flags/{id}/PUT /v1/admin/feature-flags/{id}/detail, and the platform provider kill-switches - GraphQLadminSetProviderEnabled/adminSetProviderSubtypeEnabled/adminToggleProvider/adminToggleProviderSubtypeand RESTPATCH /v1/admin/providers/{platform}[/{subtype}]/toggleand…/enabled, which flip the sameplatform:*flags. The provider paths add"via": "provider"plus theplatform(andsubtype) to the metadata;enabledis 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 GraphQLadminCreatePlan/adminUpdatePlan/adminDeletePlanand RESTPOST /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 GraphQLadminSetPlanFeatureand RESTPUT /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 GraphQLadminUpdateAccountPlanand RESTPATCH /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 GraphQLadminSetAccountFeature/adminClearAccountFeatureand RESTPUT /v1/admin/accounts/{id}/features/DELETE /v1/admin/accounts/{id}/features/{feature_id}. Theactionfield issetorclear.{ "account_id": "<uuid>", "feature_id": "<uuid>", "action": "set", "enabled": true } -
system:orphaned_storage_swept- a platform operator applies the orphaned extension upload sweep (GraphQLadminSweepOrphanedExtensionUploads/ RESTPOST /v1/admin/storage/orphaned-extension-uploads/sweep,apply: trueonly). Metadata carrieskind,orphaned_object_countandqueued_object_count. -
extension:purchase_auto_refunded- the Stripecheckout.session.completedwebhook refunded an undeliverable paid extension checkout. No actor. Metadata carriespurchase_id,cause(detached|not_installable),amount,currencyandstripe_refund_id. -
system:account_deleted- a platform operator deletes an account, via GraphQLadminDeleteAccountand RESTDELETE /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-scopeaccount: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 GraphQLadminSetAccountPermissionOverride/adminRemoveAccountPermissionOverrideand RESTPUT /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 metadataaccount_id(never the key column). Remove emits only when a row was actually removed. Thesetpath additionally recordsgrantedand the operator's optionalreason; 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.