Skip to main content

Events

Overview

The Events module handles platform event logging and alerting for streaming events such as followers, subscribers, cheers, raids, channel point redemptions, and tips. Events are stored in a TimescaleDB hypertable with automatic compression for efficient time-series queries. Events are broadcast in real time via Redis pub/sub to WebSocket clients for overlay alerts and dashboard panels. The module supports paginated queries with filters by type, platform, date range, and full-text search.

Architecture

Backend

  • GraphQL (apps/api/src/graphql/events.rs) -- Queries for listing/fetching events with filters. Mutation for emitting test/manual events.
  • Crate (crates/lo-events/src/) -- Core event logic including EventFilter, EventInput, EventRow types, list_events, get_event, insert_event, and broadcast_event functions.
  • Database -- Events are stored in TimescaleDB (platform_event_logs hypertable) with time-based partitioning and compression.
  • Real-time -- Events are broadcast via RedisPubSub using the GqlPubSub wrapper to all WebSocket subscribers for the account.

Frontend

  • Next.js API proxy routes call GraphQL internally.
  • A shared EventsShell component (apps/web/src/components/events-shell.tsx) renders the live feed with filters by type and platform. It backs three surfaces: the dedicated Events page (/dashboard/events), the compact panel on the Dashboard home, and the /popout/events window (cookie or popout-token auth).
  • Expired-connection alert. Events are produced by the account's channel connections, so a dead grant makes the feed stop silently. When a channel connection is flagged reconnect_required, the header's per-platform pill shows an icon-only warning marker and a non-dismissible error alert is pinned directly under the topbar, carrying a Reconnect button that starts that platform's OAuth flow in place. It is shown only to viewers holding connections:create (Owner / Administrator among the default roles). See Connections.
  • Overlay alerts consume events via WebSocket for real-time display.
  • First-time chatter & watch streak (Twitch). Both appear as rows in the feed and are toggleable in the type filter. A twitch:first_message row shows the chatter and their first message. A twitch:watch_streak row shows the viewer, a flame streak-count badge, and — resub-style — their attached chat message when they left one (otherwise Twitch's "watched N streams in a row" line). The first-time-chatter signal additionally tags the matching inline row in Multichat.

API

GraphQL Queries

Every account-facing event operation additionally requires the feature:events feature flag. On GraphQL the guard is FeatureGuard("feature:events") combined with the permission below; the four REST handlers call the same require_feature after their permission check.

QueryPermissionDescription
events(filter: EventFilterInput)feature:events + events:readPaginated event list with filters for types, platform, search, date range. Returns PaginatedEventResult!.
event(id: UUID!)feature:events + events:readGet a single event by ID (account-scoped)

GraphQL Mutations

MutationPermissionDescription
emitEvent(input: EmitEventInput!)feature:events + events:createEmit a test/manual event, insert into TimescaleDB and broadcast via pub/sub. Returns EmitEventResult!.
emitBotEvent(accountId: ID!, eventType: String!, platform: String!, username: String!, raw: JSON!)System key onlyBot-worker ingest path with an explicit account id. Broadcast-only — like a test event, it is not persisted.

REST Endpoints

All paths live under /v1. Bodies are snake_case.

MethodPathPermissionDescription
GET/v1/eventsfeature:events + events:readPaginated event list (accepts the same filters as EventFilterInput)
GET/v1/events/{id}feature:events + events:readFetch a single event by ID (account-scoped)
POST/v1/events/emitfeature:events + events:createEmit a manual event (insert + broadcast)
POST/v1/events/testfeature:events + events:createEmit a test event — broadcast only, never persisted

WebSocket

ChannelGateFeature flag
events:{account_id}events:read on the account in the channel keynone

Subscribing streams every event inserted or broadcast for that account in real time — the same payloads the events query returns. The gate is resolved by channel_gate_for / channel_feature_for in crates/lo-websocket/src/gate.rs; events carries no feature flag, so an account without feature:events can still stream (the REST/GraphQL read paths remain flag-gated).

Filter Options (EventFilterInput)

FieldTypeDescription
types[String!]Filter by event types (e.g., ["twitch:follower", "twitch:subscribe"])
platformStringFilter by platform (comma-separated for multiple)
userIdStringFilter by platform user ID (matches raw->>'user_id')
searchStringCase-insensitive text search in raw JSON data
fromStringISO 8601 timestamp -- only events after this time
toStringISO 8601 timestamp -- only events before this time
pageIntPage number (1-based, default 1)
limitIntItems per page (default 25, max 100)

Permissions

PermissionDescription
events:readRead/list events. Enforced on events, event, GET /v1/events, GET /v1/events/{id}.
events:createCreate events (test events, emit). Enforced on emitEvent, POST /v1/events/emit, POST /v1/events/test.
events:userinfoView user cards/profiles from event entries. Enforced client-side by the EventsShell component (<Gate permission="events:userinfo">); the underlying profile lookup is guarded by chat:userinfo.

There is no events:delete permission: no manual event-deletion surface exists on either protocol. Events are aged out automatically by the per-plan retention sweep (see Database below), not on demand — so the former events:delete account permission was removed in ZAF-1094 (it gated nothing).

Database

TableDatabaseDescription
platform_event_logsTimescaleDBHypertable, segmented by account_id, compressed after 7 days. Columns: id (UUID), account_id, type (the platform:action string — GraphQL exposes it as eventType), platform, external_id, raw (JSONB), created_at. Primary key (id, created_at). A partial unique index on (account_id, external_id, created_at) where external_id IS NOT NULL makes webhook re-delivery idempotent.

Retention

Events are retained on the same per-plan window as chat messages — the privacy policy groups "chat messages and events" under one rolling, plan-dependent window. A background sweep (the chat/event retention worker, gated by chat.retention_enforcement_enabled) hard-deletes each account's platform_event_logs rows older than its effective plans.chat_retention_days window (a per-account account_limits.chat_retention_days override takes precedence). Because the window is per-plan, it is enforced by a per-account DELETE sweep rather than a table-wide TimescaleDB retention policy. An account whose effective window is 0 keeps its events indefinitely.

Event Types

Events follow the platform:action naming convention. There are two distinct lists and they are not identical — quote the right one for your use case.

Extension-subscribable catalogue

LUMIO_EVENT_TYPES in packages/protocol/src/events.ts is the closed enum an extension may pass to useLumioEvent(). It is validated by a Zod schema, so a type outside this list is rejected at the SDK boundary:

  • Twitch: twitch:follower, twitch:subscribe, twitch:gift_sub, twitch:resub, twitch:cheer, twitch:raid, twitch:reward, twitch:hype_train, twitch:hype_train_end, twitch:poll, twitch:poll_end, twitch:prediction, twitch:prediction_lock, twitch:prediction_end, twitch:goal, twitch:goal_end, twitch:ad_break, twitch:ban, twitch:unban, twitch:stream_online, twitch:stream_offline, twitch:stream_update
  • YouTube: youtube:subscribe, youtube:member, youtube:superchat, youtube:supersticker, youtube:gift_membership, youtube:gift_membership_received, youtube:poll
  • Kick: kick:follower, kick:subscribe, kick:gift, kick:stream_online, kick:stream_offline
  • Trovo: trovo:subscribe, trovo:spell
  • Spotify: spotify:track, spotify:play, spotify:pause, spotify:skip, spotify:volume, spotify:queue_add, spotify:device, spotify:playlist_add, spotify:playlist_remove, spotify:playlist_create, spotify:playlist_edit, spotify:playlist_delete
  • StreamElements: streamelements:tip
  • Chat (per-platform): twitch:chat, youtube:chat, kick:chat, trovo:chat
  • Chat (cross-platform): chat:message (fires for all platforms)

Types the platform workers actually emit

The type column on a platform_event_logs row is a free-form VARCHAR(100), so the ingest workers write several types that are not in the extension catalogue above:

SourceAdditional emitted types
Twitch EventSub (workers/twitch_eventsub.rs)twitch:gift (sub gifts — the worker's name for what the extension catalogue calls twitch:gift_sub), twitch:gift_upgrade, twitch:prime_upgrade, twitch:pay_it_forward, twitch:bits_badge, twitch:poll_delete, twitch:raid_out, twitch:raid_done, twitch:unraid
Twitch IRC-tags listener (workers/twitch_irc_tags.rs)twitch:first_message (first-time chatter — PRIVMSG first-msg=1), twitch:watch_streak (USERNOTICE viewermilestone / watch-streak, carries streak_count)
Trovo (workers/trovo.rs)trovo:follower, trovo:raid, trovo:subscriber
Discord bot (apps/discord-bot)discord:member_join, discord:member_leave, discord:boost

Conversely, twitch:ban and twitch:unban appear in the extension catalogue but are not written to platform_event_logs — Twitch ban/unban is handled on the chat path (see Chat).

Internal WebSocket channel messages

These share the namespace:action shape but are transient pub/sub payloads, not platform_event_logs rows: chat:message, chat:delete, chat:clear_user, chat:moderation_log, chat:user_treatment_update, channel:online, channel:offline, copyright:blocked/:warning/:clear, obs:action/:connection_lost/:low_bitrate/:high_dropped_frames/:recovery, history:session_started/:session_updated, sound:play, overlay:update, widget:update, storage:update, extension:updated.

Data Flow

  1. Platform adapter detects an event (e.g., Twitch EventSub webhook receives a follow).
  2. Event is inserted into TimescaleDB via lo_events::insert_event().
  3. Event is broadcast via lo_events::broadcast_event() through Redis pub/sub.
  4. WebSocket server forwards the event to all connected clients for the account.
  5. Overlay alerts render the event. The Events page, home event panel, and popout update in real time.
  6. Users can query historical events with filters and pagination via events query.

Key Files

PathDescription
apps/api/src/graphql/events.rsGraphQL queries and mutations
apps/api/src/routes/events.rsREST endpoints
crates/lo-events/src/Core events crate — TimescaleDB insert/list plus Redis broadcast and the filter types
apps/api/src/workers/event_relay.rsRelays platform-worker events onto the account's WebSocket channel
packages/protocol/src/events.tsLUMIO_EVENT_TYPES / ACTION_TYPES enums consumed by the extension SDK
apps/web/src/components/events-shell.tsxShared live-feed component behind the Events page, dashboard panel, and popout