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
| Platform | Auth Type | Token Refresh |
|---|---|---|
| Twitch | OAuth2 | Yes (automatic) |
| YouTube | OAuth2 | Yes (automatic) |
| Kick | OAuth2 | Yes (automatic) |
| Trovo | OAuth2 | Yes (automatic) |
| Discord | Static Bot Token | No (permanent) |
Bot Scopes
Bot connections use minimal scopes focused on chat functionality:
| Platform | Scopes |
|---|---|
| Twitch | chat:read, chat:edit, user:write:chat, user:bot, user:read:chat |
| YouTube | youtube.force-ssl |
| Kick | chat:write |
| Trovo | chat_send_self |
| Discord | Set 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 thebot_connectionstable. 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
| Query | Permission | Description |
|---|---|---|
botConnections | bot-connections:read | List bot connections for the account |
botConnectionStatuses | bot-connections:read | Bot platform statuses with enabled field |
botStatus | bot-connections:read | Per-platform bot status for the Bot Control dashboard |
botChannels(platform) | System key only | All bot-enabled channels for a platform (consumed by bot workers) |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
authorizeBot(platform) | bot-connections:create | Start a bot OAuth flow. Returns AuthorizeResult (redirect URL). Discord is unsupported (static token). |
deleteBotConnection(platform) | bot-connections:delete | Delete a bot connection |
toggleBotForPlatform(platform, enabled) | bot-connections:create | Toggle bot_enabled on the platform's channel connection |
rejoinBot(platform) | bot-connections:create | Send a rejoin command to the bot worker for a platform |
REST Endpoints
All paths live under /v1.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/bot-connections | bot-connections:read | List bot connections for the account |
DELETE | /v1/bot-connections/{platform} | bot-connections:delete | Delete a bot connection |
GET | /v1/bot-connections/{platform}/authorize | bot-connections:create | Start OAuth and return the authorize URL |
POST | /v1/bot-connections/{platform}/exchange | bot-connections:create | Exchange the OAuth code for encrypted tokens |
GET | /v1/bot-status | bot-connections:read | Aggregated bot runtime status for the account |
POST | /v1/bot-toggle | bot-connections:create | Pause or resume the bot for a platform |
POST | /v1/bot-rejoin | bot-connections:create | Force 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:
| Bot | Fields requested | Uses them for |
|---|---|---|
| twitch-bot | accountId, channelName, botType, customBotToken | Global vs custom bot selection |
| youtube-bot | accountId, channelName, accessToken | Live-chat poll + send |
| trovo-bot | accountId, channelName, channelId, accessToken | Chat-WS auth frame + send |
| kick-bot | accountId, channelName | Discovery 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, sobotChannelscannot supply it. The kick-bot therefore discovers its channels but subscribes to none, logging awarn!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 byapps/apiis 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 theapps/apisend 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
| Permission | Owner | Admin | Moderator | Viewer |
|---|---|---|---|---|
bot-connections:read | Yes | Yes | Yes | No |
bot-connections:create | Yes | Yes | No | No |
bot-connections:delete | Yes | Yes | No | No |
Database
Bot connections are stored in the bot_connections table:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
account_id | UUID | Account (nil UUID for global bots) |
platform | VARCHAR | Platform name |
bot_type | VARCHAR | global or custom |
bot_username | VARCHAR | Bot display name |
access_token | TEXT | Encrypted OAuth token |
refresh_token | TEXT | Encrypted refresh token |
scopes | TEXT[] | Granted scopes |
expires_at | TIMESTAMPTZ | Token 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
| Path | Description |
|---|---|
apps/api/src/graphql/bot_connections.rs | GraphQL queries and mutations |
apps/api/src/db/bot_connections.rs | Database operations |
apps/api/src/oauth.rs | Token refresh utilities (get_fresh_bot_token, update_bot_connection_tokens) |
apps/api/src/platforms.rs | Bot OAuth scopes per platform |
See Also
- Provider Management -- Feature flag controls for bot connections
- Connections -- Channel connection OAuth flow (similar pattern)
- Token Refresh -- Automatic token refresh worker
- Bot Modules -- Chat moderation modules that use bot connections