Skip to main content

Bot Connections

Overview

Bot connections allow accounts to use custom bot identities instead of the global system bot. Each account can connect their own bot account via OAuth for any supported chat platform. The system supports both global bots (configured by admins) and per-account custom bots.

Global vs Custom Bots

  • Global Bot: Configured by admins in the Provider Management page. All accounts use this bot by default.
  • Custom Bot: Users connect their own bot account via OAuth. Takes priority over the global bot for that account.

Fallback Logic

Account has custom bot connection for platform?
-> YES: Use custom bot token
-> NO: Use global system bot token

Platforms

PlatformAuth TypeToken Refresh
TwitchOAuth2Yes (automatic)
YouTubeOAuth2Yes (automatic)
KickOAuth2Yes (automatic)
TrovoOAuth2Yes (automatic)
DiscordStatic Bot TokenNo (permanent)

Bot Scopes

Bot connections use minimal scopes focused on chat functionality:

PlatformScopes
Twitchchat:read, chat:edit, user:write:chat, user:bot, user:read:chat
YouTubeyoutube.force-ssl
Kickchat:write
Trovochat_send_self
DiscordSet via Developer Portal (not OAuth scopes)

Architecture

Backend

  • GraphQL (apps/api/src/graphql/bot_connections.rs) -- Queries for listing bot connections, bot connection statuses with enablement. Mutation for deleting bot connections.
  • REST -- Bot connection OAuth flow follows the same pattern as channel connections with dedicated callback URLs.
  • Database (apps/api/src/db/bot_connections.rs) -- PostgreSQL operations for the bot_connections table. Supports list, get, upsert, and delete operations.
  • OAuth (apps/api/src/oauth.rs) -- get_fresh_bot_token() for reading fresh tokens, update_bot_connection_tokens() for saving refreshed tokens.

Frontend

  • Bot connection settings page with cards per platform showing connection status.
  • "Connect" button initiates OAuth flow with bot-specific scopes.
  • Separate from channel connections UI.

API

GraphQL Queries

QueryPermissionDescription
botConnectionsbot-connections:readList bot connections for the account
botConnectionStatusesbot-connections:readBot platform statuses with enabled field
botStatusbot-connections:readPer-platform bot status for the Bot Control dashboard
botChannels(platform)System key onlyAll bot-enabled channels for a platform (consumed by bot workers)

GraphQL Mutations

MutationPermissionDescription
authorizeBot(platform)bot-connections:createStart a bot OAuth flow. Returns AuthorizeResult (redirect URL). Discord is unsupported (static token).
deleteBotConnection(platform)bot-connections:deleteDelete a bot connection
toggleBotForPlatform(platform, enabled)bot-connections:createToggle bot_enabled on the platform's channel connection
rejoinBot(platform)bot-connections:createSend a rejoin command to the bot worker for a platform

REST Endpoints

All paths live under /v1.

MethodPathPermissionDescription
GET/v1/bot-connectionsbot-connections:readList bot connections for the account
DELETE/v1/bot-connections/{platform}bot-connections:deleteDelete a bot connection
GET/v1/bot-connections/{platform}/authorizebot-connections:createStart OAuth and return the authorize URL
POST/v1/bot-connections/{platform}/exchangebot-connections:createExchange the OAuth code for encrypted tokens
GET/v1/bot-statusbot-connections:readAggregated bot runtime status for the account
POST/v1/bot-togglebot-connections:createPause or resume the bot for a platform
POST/v1/bot-rejoinbot-connections:createForce the bot to reconnect / rejoin the channel

Request and response bodies use snake_case and mirror the GraphQL counterparts.

Channel Discovery (bot workers)

Each bot worker polls botChannels(platform) (system-key only) to learn which channels to join. The returned BotChannel carries accountId, channelName, channelId, botEnabled, botType, customBotToken, and accessToken — the decrypted channel OAuth token, exposed only over this system-key-gated query (the same trust boundary as customBotToken).

Each worker requests only the fields it needs:

BotFields requestedUses them for
twitch-botaccountId, channelName, botType, customBotTokenGlobal vs custom bot selection
youtube-botaccountId, channelName, accessTokenLive-chat poll + send
trovo-botaccountId, channelName, channelId, accessTokenChat-WS auth frame + send
kick-botaccountId, channelNameDiscovery only (see below)

A worker must request only fields BotChannel actually exposes: GraphQL rejects the entire query on an unknown field, which leaves the worker subscribed to zero channels. The struct must also carry #[serde(rename_all = "camelCase")] or the camelCase response fails to deserialize. Both traps are guarded by apps/api/tests/bot_channel_queries.rs and per-bot deserialization tests.

Worker health signal

A channel sync that fails repeatedly is surfaced, not silent: each worker's /ready endpoint returns 503 once channel sync has failed several times in a row, and reports channels_active, channels_skipped, and consecutive_sync_failures as JSON. /health (liveness) stays OK while the process runs.

Known Exceptions & Operational Risk

  • Kick live subscription is deferred. The kick-bot subscribes to chat over Kick's Pusher WebSocket keyed by a chatroom_id, which Lumio does not store server-side, so botChannels cannot supply it. The kick-bot therefore discovers its channels but subscribes to none, logging a warn! with a skip counter (reflected on /ready). The Pusher path also uses a hard-coded public app key with no auth frame — an unofficial channel with no platform contract; the official Kick chat ingest used by apps/api is the documented webhook. Building out Kick bot chat is tracked separately.
  • Trovo bot Client-ID. The trovo-bot sends chat with a literal Client-ID: "lumio-bot" rather than the account's app-credential Client-ID used by the apps/api send path — two sources of truth for the same value. Deliberate for now; align it with the configured credential when Trovo bot chat is built out.

Permissions

PermissionOwnerAdminModeratorViewer
bot-connections:readYesYesYesNo
bot-connections:createYesYesNoNo
bot-connections:deleteYesYesNoNo

Database

Bot connections are stored in the bot_connections table:

ColumnTypeDescription
idUUIDPrimary key
account_idUUIDAccount (nil UUID for global bots)
platformVARCHARPlatform name
bot_typeVARCHARglobal or custom
bot_usernameVARCHARBot display name
access_tokenTEXTEncrypted OAuth token
refresh_tokenTEXTEncrypted refresh token
scopesTEXT[]Granted scopes
expires_atTIMESTAMPTZToken expiration

Token Refresh

The Token Refresh Worker automatically refreshes bot connection tokens alongside channel and login connection tokens. Discord bot tokens are excluded since they don't expire. See Token Refresh for details.

Feature Flags

Bot connections are gated by platform:{platform}:bot feature flags. When the bot sub-flag is disabled for a platform, new bot connections are blocked but existing ones continue working. See Provider Management for details.

Key Files

PathDescription
apps/api/src/graphql/bot_connections.rsGraphQL queries and mutations
apps/api/src/db/bot_connections.rsDatabase operations
apps/api/src/oauth.rsToken refresh utilities (get_fresh_bot_token, update_bot_connection_tokens)
apps/api/src/platforms.rsBot OAuth scopes per platform

See Also