OAuth Provider Setup
This guide walks you through creating OAuth applications for each platform. Lumio uses distinct credential sources for login, channel, and bot flows. They are stored in different places and are not interchangeable:
| Login credentials | System channel credentials | System bot credentials | Account channel/bot credentials | |
|---|---|---|---|---|
| Purpose | User authentication ("Sign in with Twitch") | Lumio-managed channel OAuth apps | Lumio-managed bot OAuth apps | Per-account platform API access (chat, events, moderation) |
| Stored in | apps/id/.env.local and apps/api/config/local.toml | [auth] *_channel_client_id / *_channel_client_secret keys in the API | [auth] *_bot_client_id / *_bot_client_secret keys in the API | The app_credentials database table (AES-256-GCM encrypted) |
| Entered by | The operator, once, in config files | The operator, once per environment / provider app | The operator, once per environment / provider app | Each account, in Dashboard → Connections → App Credentials |
| Scopes | User-level, defined in apps/id/src/auth.ts | Channel scopes, defined in apps/api/src/platforms.rs | Bot scopes, defined in apps/api/src/platforms.rs | Channel/bot scopes, defined in apps/api/src/platforms.rs |
| Resulting record | login_connections | channel_connections, pinned credential_source = "system" | bot_connections, pinned credential_source = "system" | channel_connections or bot_connections, pinned credential_source = "account" |
Login credentials are not channel credentials, and channel credentials are not bot credentials. System channel and bot credentials are the global Lumio OAuth apps used only by connections pinned to system; account credentials are used only by connections pinned to account. The login scopes in apps/id/src/auth.ts are user-level — they are not a subset of the channel scopes and must not be stripped on the assumption that they belong on the channel side.
A third surface — bot connections — is covered in Bot Connections at the end of this guide.
Redirect URI Registration Matrix
System-sourced channel connections, account bot connections, and admin global-bot connections use the unified web-host callback:
{web.public_url}/api/connections/oauth/{platform}/callback
For production Twitch/Kick/Trovo system apps, register:
https://lumio.vision/api/connections/oauth/{platform}/callback
For staging, register:
https://lumio.web.staging.zaflun.dev/api/connections/oauth/{platform}/callback
For local development, register:
http://localhost:4000/api/connections/oauth/{platform}/callback
The server dispatches the callback by the OAuth state handle's kind (channel, bot, or global_bot) and return_to. Account/BYO channel connections and degraded bot flows still use source-specific legacy callback URLs because their authorize path emits the legacy state format.
Login apps
Login OAuth belongs to the ID app. These apps are separate from system channel/bot apps.
| Platform | Production redirect URI | Staging redirect URI | Local redirect URI | Notes |
|---|---|---|---|---|
| Twitch login | https://id.lumio.vision/api/auth/callback/twitch | https://lumio.id.staging.zaflun.dev/api/auth/callback/twitch | http://localhost:4002/api/auth/callback/twitch | Twitch allows multiple redirect URIs per app, so the login pair can also serve as the Twitch channel fallback when the channel pair is unset and the app also registers the unified web callback. |
| YouTube / Google login | https://id.lumio.vision/api/auth/callback/google | https://lumio.id.staging.zaflun.dev/api/auth/callback/google | http://localhost:4002/api/auth/callback/google | The ID app provider slug is google; channel connections use the Lumio platform slug youtube. |
| Kick login | https://id.lumio.vision/api/auth/callback/kick | https://lumio.id.staging.zaflun.dev/api/auth/callback/kick | http://localhost:4002/api/auth/callback/kick | Kick accepts one redirect URI per app, so the login app cannot be reused for channel or bot system OAuth. Use separate apps for hosted and local development. |
| Trovo login | https://id.lumio.vision/api/auth/callback/trovo | https://lumio.id.staging.zaflun.dev/api/auth/callback/trovo | http://localhost:4002/api/auth/callback/trovo | Trovo accepts one redirect URI per app, so the login app cannot be reused for channel or bot system OAuth. Use separate apps for hosted and local development. |
System channel and bot apps
System OAuth app pairs live in the API [auth] config. Channel apps use *_channel_client_id / *_channel_client_secret; dedicated bot apps use *_bot_client_id / *_bot_client_secret. The Channel app and Bot app for the same provider register the same callback in a given environment; the encrypted state handle selects whether Lumio redeems the code with the channel, account-bot, or admin global-bot secret pair.
| Platform / app | Production redirect URI | Staging redirect URI | Local redirect URI | Notes |
|---|---|---|---|---|
| Twitch Channel app | https://lumio.vision/api/connections/oauth/twitch/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/twitch/callback | http://localhost:4000/api/connections/oauth/twitch/callback | Twitch supports multiple redirect URIs on one app. A single Twitch Channel app can carry production, staging and local callbacks when that matches the operator model. |
| Twitch Bot app | https://lumio.vision/api/connections/oauth/twitch/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/twitch/callback | http://localhost:4000/api/connections/oauth/twitch/callback | Dedicated bot keys are preferred. If absent, bot connects fall back to the Twitch channel pair, then to the Twitch login pair. |
| Kick Channel app | https://lumio.vision/api/connections/oauth/kick/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/kick/callback | http://localhost:4000/api/connections/oauth/kick/callback | Kick accepts one redirect URI per app. Use one Channel app per environment, including a separate local development app. |
| Kick Bot app | https://lumio.vision/api/connections/oauth/kick/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/kick/callback | http://localhost:4000/api/connections/oauth/kick/callback | The one-URI limit is per app, so the Channel app and Bot app may each register the same environment callback. |
| Trovo Channel app | https://lumio.vision/api/connections/oauth/trovo/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/trovo/callback | http://localhost:4000/api/connections/oauth/trovo/callback | Trovo accepts one redirect URI per app. Use one Channel app per environment, including a separate local development app. |
| Trovo Bot app | https://lumio.vision/api/connections/oauth/trovo/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/trovo/callback | http://localhost:4000/api/connections/oauth/trovo/callback | The one-URI limit is per app, so the Channel app and Bot app may each register the same environment callback. |
| YouTube Bot app | https://lumio.vision/api/connections/oauth/youtube/callback | https://lumio.web.staging.zaflun.dev/api/connections/oauth/youtube/callback | http://localhost:4000/api/connections/oauth/youtube/callback | YouTube channel connections remain BYO/account. The system callback is only for an operator-managed YouTube bot app. |
Account/BYO callbacks
Account-owned credentials are entered per account in Dashboard → Connections → App Credentials and are stored in the database. These callbacks are legacy/BYO callbacks: register them only for account-managed connections, for local setups intentionally using account credentials, or for platforms that have degraded from system to account because no system pair is configured.
| Platform / flow | Production redirect URI | Staging redirect URI | Local redirect URI | Notes |
|---|---|---|---|---|
| Twitch BYO channel | https://lumio.vision/api/connections/channel/twitch/callback | https://lumio.web.staging.zaflun.dev/api/connections/channel/twitch/callback | http://localhost:4000/api/connections/channel/twitch/callback | Needed only when Twitch channel connects use account credentials. |
| YouTube BYO channel | https://lumio.vision/api/connections/channel/youtube/callback | https://lumio.web.staging.zaflun.dev/api/connections/channel/youtube/callback | http://localhost:4000/api/connections/channel/youtube/callback | YouTube channel connections are BYO/account; the platform slug is youtube, not google. |
| Kick BYO channel | https://lumio.vision/api/connections/channel/kick/callback | https://lumio.web.staging.zaflun.dev/api/connections/channel/kick/callback | http://localhost:4000/api/connections/channel/kick/callback | Needed only when Kick channel connects use account credentials. |
| Trovo BYO channel | https://lumio.vision/api/connections/channel/trovo/callback | https://lumio.web.staging.zaflun.dev/api/connections/channel/trovo/callback | http://localhost:4000/api/connections/channel/trovo/callback | Needed only when Trovo channel connects use account credentials. |
| Spotify BYO channel | https://lumio.vision/api/connections/channel/spotify/callback | https://lumio.web.staging.zaflun.dev/api/connections/channel/spotify/callback | http://127.0.0.1:4000/api/connections/channel/spotify/callback | Spotify rejects localhost; Lumio rewrites localhost to 127.0.0.1 only for Spotify local callbacks. |
| Account bot legacy callback | https://lumio.vision/api/connections/bot/{platform}/callback | https://lumio.web.staging.zaflun.dev/api/connections/bot/{platform}/callback | http://localhost:4000/api/connections/bot/{platform}/callback | Used only when an account bot flow degrades to account credentials instead of using a system bot/channel pair. |
| Admin global-bot legacy callback | https://admin.lumio.vision/api/bot-connections/{platform}/callback | https://lumio.admin.staging.zaflun.dev/api/bot-connections/{platform}/callback | http://localhost:4001/api/bot-connections/{platform}/callback | Kept for legacy/in-flight global bot authorizations. Current system global bots return through the web-host unified callback and then redirect back to Admin via return_to. |
Production needs six system apps for the Twitch, Kick, and Trovo system-managed paths: one Channel app and one Bot app per platform. A production + staging setup uses ten apps: Twitch can register both production and staging redirect URIs on each of its two apps, while Kick and Trovo need separate Channel and Bot apps per environment.
| Platform | Production system apps | Staging system apps | System callback |
|---|---|---|---|
| Twitch | 2 apps: Channel + Bot | same 2 apps | https://lumio.vision/api/connections/oauth/twitch/callback and https://lumio.web.staging.zaflun.dev/api/connections/oauth/twitch/callback |
| Kick | 2 apps: Channel + Bot | 2 apps: Channel + Bot | one callback per environment: .../api/connections/oauth/kick/callback |
| Trovo | 2 apps: Channel + Bot | 2 apps: Channel + Bot | one callback per environment: .../api/connections/oauth/trovo/callback |
Kick and Trovo limit redirect URIs per app, not apps per URI. Their Channel app and Bot app both register the same environment callback, for example https://lumio.vision/api/connections/oauth/kick/callback; the state.kind value selects whether Lumio uses the channel secret pair or the bot secret pair. Their login app cannot fall back into channel or bot system mode because its only URI is {ID_APP_URL}/api/auth/callback/{platform}.
YouTube bot credentials have dedicated [auth] key slots too (youtube_bot_client_id / youtube_bot_client_secret), but YouTube channel connections stay BYO/account and there is no YouTube channel system app to fall back to. Without the YouTube bot keys, the YouTube bot flow degrades to account.
Twitch
Developer Console
Login Credentials (ID App)
- Go to Applications → Register Your Application
- Set Name to something like
Lumio Login - Set OAuth Redirect URL to:
{ID_APP_URL}/api/auth/callback/twitch- Local:
http://localhost:4002/api/auth/callback/twitch
- Local:
- Set Category to
Website Integration - Copy Client ID and generate a Client Secret
- Add to
apps/id/.env.local:AUTH_TWITCH_ID=your_client_idAUTH_TWITCH_SECRET=your_client_secret
Login Scopes (requested automatically):
| Scope | Purpose |
|---|---|
openid | OpenID Connect authentication |
user:read:email | Read user email |
user:read:emotes | Read user emotes |
chat:read | Read chat messages |
chat:edit | Send chat messages |
user:write:chat | Write chat messages |
user:read:subscriptions | Read user subscriptions |
moderator:read:chatters | Read chatter list |
moderator:manage:chat_messages | Delete chat messages |
moderator:manage:banned_users | Ban/unban users |
Channel Credentials
Twitch channel connections default to system. Configure a global Twitch channel app in the API [auth] config (twitch_channel_client_id / twitch_channel_client_secret) and register the unified callback:
{web.public_url}/api/connections/oauth/twitch/callback
Twitch allows multiple redirect URIs on one app, so if the channel-specific pair is empty, Lumio may use the Twitch login pair as the system pair. If no global pair is available, new Twitch channel connects degrade to the account-credential path and use the legacy account callback:
{WEB_URL}/api/connections/channel/twitch/callback
Channel Scopes (27 scopes, requested automatically):
| Scope | Purpose |
|---|---|
channel:read:subscriptions | Read subscriber list and events |
channel:read:redemptions | Read channel point redemptions |
channel:manage:redemptions | Manage channel point redemptions |
channel:read:hype_train | Read hype train events |
channel:read:polls | Read poll data |
channel:manage:polls | Create and manage polls |
channel:read:predictions | Read prediction data |
channel:manage:predictions | Create and manage predictions |
channel:read:goals | Read channel goals |
bits:read | Read cheer/bits events |
moderator:read:followers | Read follower list |
moderator:read:suspicious_users | Read suspicious user flags |
moderator:manage:suspicious_users | Update suspicious user treatment |
moderator:manage:banned_users | Ban and unban users |
channel:bot | Send chat messages as the channel bot |
user:read:chat | Read chat messages |
channel:read:ads | Read ad schedule |
channel:manage:raids | Start and cancel raids |
channel:moderate | Moderate chat |
moderator:read:blocked_terms | Read blocked terms |
moderator:read:chat_settings | Read chat settings |
moderator:read:unban_requests | Read unban requests |
moderator:read:banned_users | Read banned users list |
moderator:read:chat_messages | Read chat messages as moderator |
moderator:read:warnings | Read user warnings |
moderator:read:moderators | Read moderator list |
moderator:read:vips | Read VIP list |
Bot Credentials
Twitch bot connections default to system. Configure a separate Twitch bot app in [auth] with twitch_bot_client_id / twitch_bot_client_secret and register the same unified callback as the channel app:
{web.public_url}/api/connections/oauth/twitch/callback
If the Twitch bot pair is empty, the bot flow falls back to the Twitch channel pair, then to the Twitch login pair, and logs a one-time warning. If neither bot nor channel/login keys are available, the bot flow degrades to account.
YouTube / Google
Developer Console
https://console.cloud.google.com/apis/credentials
Prerequisites
- Create a Google Cloud project (or use an existing one)
- Enable YouTube Data API v3 under APIs & Services > Library
- Configure OAuth consent screen under APIs & Services > OAuth consent screen
- User Type: External (or Internal for Google Workspace)
- Add the scopes listed below
- Add test users if app is in "Testing" status
Login Credentials (ID App)
- Go to APIs & Services > Credentials → Create Credentials → OAuth 2.0 Client ID
- Application type: Web application
- Set Authorized redirect URI to:
{ID_APP_URL}/api/auth/callback/google- Local:
http://localhost:4002/api/auth/callback/google
- Local:
- Copy Client ID and Client Secret
- Add to
apps/id/.env.local:AUTH_GOOGLE_ID=your_client_idAUTH_GOOGLE_SECRET=your_client_secret
Login Scopes:
| Scope | Purpose |
|---|---|
openid | OpenID Connect authentication |
email | User email address |
profile | User name and profile picture |
youtube.readonly | Read YouTube channel data — also used at login to fetch snippet.customUrl for the username |
youtube.force-ssl | YouTube Data API access (live chat) |
Username resolution. Right after the OAuth handshake the ID App calls youtube/v3/channels?part=snippet&mine=true with the user's fresh access token and stores snippet.customUrl (e.g. cruex_de) as the login connection's username. Without this lookup Google's OIDC profile only exposes email (typically a …@pages.plusgoogle.com pseudo-email), which would never match the YouTube display name a viewer actually @-mentions in chat. If the YouTube call fails (no channel attached to the Google account, scope revoked) we fall back to email. The handle drives both the multichat @-mention highlighter and the broadcaster-status fallback that unlocks moderation buttons before the broadcaster's first chat message.
Channel Credentials
YouTube channel connections are BYO/account. Each account supplies its own Google Cloud OAuth client.
- Create a separate OAuth 2.0 Client ID in the same Google Cloud project
- Set Authorized redirect URI to:
{WEB_URL}/api/connections/channel/youtube/callback- Local:
http://localhost:4000/api/connections/channel/youtube/callback - The channel-connection platform slug is
youtube, notgoogle— only the ID App's login callback usesgoogle.
- Local:
- Users enter their own Client ID + Secret in Dashboard → Connections → App Credentials
Channel Scopes (2 scopes):
| Scope | Purpose |
|---|---|
youtube.readonly | Read channel, video, and live chat data |
youtube.force-ssl | Full YouTube Data API access (chat, comments) |
Both login and channel credentials request access_type=offline and prompt=consent to ensure refresh tokens are always issued.
Bot Credentials
YouTube bot connections have a system credential mode but no channel-system app to fall back to. Configure youtube_bot_client_id / youtube_bot_client_secret in [auth] and register the unified callback when the deployment uses an operator-managed YouTube bot app:
{web.public_url}/api/connections/oauth/youtube/callback
If the YouTube bot pair is empty, the YouTube bot flow degrades to account.
Discord
Developer Console
https://discord.com/developers/applications
Login Credentials
Discord has no channel connection — it is a login provider, a bot platform, and a guild integration. Only the login flow uses OAuth credentials from config; the Discord bot authenticates with a static bot token instead of OAuth.
- Go to Applications → New Application
- Under OAuth2, copy Client ID and Client Secret
- Add Redirect URL:
{ID_APP_URL}/api/auth/callback/discord- Local:
http://localhost:4002/api/auth/callback/discord
- Local:
- Add to
apps/id/.env.local:AUTH_DISCORD_ID=your_client_idAUTH_DISCORD_SECRET=your_client_secret
Login Scopes:
| Scope | Purpose |
|---|---|
identify | Read user identity |
email | Read user email |
guilds | Read server list |
messages.read | Read messages |
Kick
Developer Console
https://kick.com/settings/developer
Login Credentials (ID App)
- Create a new application in the Kick Developer settings
- Set Redirect URL to:
{ID_APP_URL}/api/auth/callback/kick- Local:
http://localhost:4002/api/auth/callback/kick
- Local:
- Copy Client ID and Client Secret
- Add to
apps/id/.env.local:AUTH_KICK_ID=your_client_idAUTH_KICK_SECRET=your_client_secret
Login Scopes:
| Scope | Purpose |
|---|---|
user:read | Read user profile |
chat:write | Send chat messages |
Channel Credentials
Kick channel connections default to system. Create a separate app per environment and register that environment's unified callback:
{web.public_url}/api/connections/oauth/kick/callback
Set kick_channel_client_id and kick_channel_client_secret in the API [auth] config. There is no fallback to the Kick login credentials because the login app's single redirect URI is already {ID_APP_URL}/api/auth/callback/kick. If no global Kick channel pair is configured, new connects degrade to the account-credential path and use the legacy account callback:
{WEB_URL}/api/connections/channel/kick/callback
Channel Scopes (6 scopes):
| Scope | Purpose |
|---|---|
user:read | Read user profile information |
channel:read | Read channel information |
events:read | Read channel events |
events:subscribe | Subscribe to real-time events |
chat:write | Send chat messages |
moderation:chat_message:manage | Delete chat messages |
Bot Credentials
Kick bot connections default to system. Create a separate Kick Bot app per environment, register the same unified callback as the Kick Channel app, and set kick_bot_client_id / kick_bot_client_secret in the API [auth] config. The one-URI limit is per Kick app, so the Channel app and Bot app can both use:
{web.public_url}/api/connections/oauth/kick/callback
If the Kick bot pair is empty, the bot flow falls back to the Kick channel pair and logs a one-time warning. If neither pair is available, the bot flow degrades to account.
Trovo
Developer Console
https://developer.trovo.live/myconsole
Login Credentials (ID App)
- Create a new application in the Trovo Developer Console
- Set Redirect URL to:
{ID_APP_URL}/api/auth/callback/trovo- Local:
http://localhost:4002/api/auth/callback/trovo
- Local:
- Copy Client ID and Client Secret
- Add to
apps/id/.env.local:AUTH_TROVO_ID=your_client_idAUTH_TROVO_SECRET=your_client_secret
Login Scopes:
| Scope | Purpose |
|---|---|
user_details_self | Read own user details |
chat_send_self | Send chat messages |
Channel Credentials
Trovo channel connections default to system. Create a separate app per environment and register that environment's unified callback:
{web.public_url}/api/connections/oauth/trovo/callback
Set trovo_channel_client_id and trovo_channel_client_secret in the API [auth] config. There is no fallback to the Trovo login credentials because the login app's single redirect URI is already {ID_APP_URL}/api/auth/callback/trovo. If no global Trovo channel pair is configured, new connects degrade to the account-credential path and use the legacy account callback:
{WEB_URL}/api/connections/channel/trovo/callback
Channel Scopes (2 scopes):
| Scope | Purpose |
|---|---|
channel_details_self | Read own channel details |
channel_subscriptions | Read subscriber data |
Bot Credentials
Trovo bot connections default to system. Create a separate Trovo Bot app per environment, register the same unified callback as the Trovo Channel app, and set trovo_bot_client_id / trovo_bot_client_secret in the API [auth] config:
{web.public_url}/api/connections/oauth/trovo/callback
If the Trovo bot pair is empty, the bot flow falls back to the Trovo channel pair and logs a one-time warning. If neither pair is available, the bot flow degrades to account.
Spotify
Developer Console
https://developer.spotify.com/dashboard
Channel Credentials Only
Spotify is channel connection only — there is no Spotify login.
- Go to Dashboard → Create App
- Set Redirect URI to:
{WEB_URL}/api/connections/channel/spotify/callback- Local:
http://127.0.0.1:4000/api/connections/channel/spotify/callback - Spotify rejects
localhostredirect URIs, so Lumio rewriteslocalhost→127.0.0.1for Spotify (and only Spotify) when building the callback URL. Register the127.0.0.1form.
- Local:
- Under "Which API/SDKs are you planning to use?", select both Web API and Web Playback SDK
- Users enter their own Client ID + Secret in Dashboard → Connections → App Credentials
Channel Scopes (7 scopes):
| Scope | Purpose |
|---|---|
user-read-playback-state | Read current playback state |
user-modify-playback-state | Control playback (play, pause, skip) |
user-read-currently-playing | Read currently playing track |
playlist-read-private | Read private playlists |
playlist-read-collaborative | Read collaborative playlists |
playlist-modify-public | Modify public playlists |
playlist-modify-private | Modify private playlists |
Spotify uses Authorization: Basic base64(client_id:client_secret) for token refresh, unlike other platforms which send credentials in the POST body.
Where to Configure
Login credentials must be set in two places — the ID App's .env.local and the API's config/local.toml. The API needs them for login token refresh and Twitch chat API calls.
1. ID App (apps/id/.env.local)
Used by NextAuth for the OAuth login flow:
# NextAuth
AUTH_SECRET=generate-a-random-secret-here
AUTH_URL=http://localhost:4002
# Twitch
AUTH_TWITCH_ID=
AUTH_TWITCH_SECRET=
# Discord
AUTH_DISCORD_ID=
AUTH_DISCORD_SECRET=
# Google / YouTube
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# Kick
AUTH_KICK_ID=
AUTH_KICK_SECRET=
# Trovo
AUTH_TROVO_ID=
AUTH_TROVO_SECRET=
2. API Server (apps/api/config/local.toml)
local.toml is git-ignored and is the last file layer before environment variables (default.toml → {env}.toml → local.toml → ENV). The API uses these for login token refresh and Twitch chat operations. They must match the ID App's credentials:
[auth]
# Same credentials as AUTH_TWITCH_ID / AUTH_TWITCH_SECRET in ID App
twitch_client_id = "..."
twitch_client_secret = "..."
# Same credentials as AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET in ID App
google_client_id = "..."
google_client_secret = "..."
# Same client id as AUTH_DISCORD_ID in ID App (audience validation only — see below).
discord_client_id = "..."
# Same credentials as AUTH_KICK_ID / AUTH_KICK_SECRET in ID App
kick_client_id = "..."
kick_client_secret = "..."
# Same credentials as AUTH_TROVO_ID / AUTH_TROVO_SECRET in ID App
trovo_client_id = "..."
trovo_client_secret = "..."
# Global system Channel apps. Twitch may fall back to the login pair; Kick and
# Trovo cannot because each provider app accepts one redirect URI.
twitch_channel_client_id = "..."
twitch_channel_client_secret = "..."
kick_channel_client_id = "..."
kick_channel_client_secret = "..."
trovo_channel_client_id = "..."
trovo_channel_client_secret = "..."
# Dedicated global system Bot apps. If one of these pairs is empty, that bot
# flow falls back to the platform's channel pair and logs a warning. If neither
# bot nor channel keys are configured, the bot flow degrades to account/BYO.
twitch_bot_client_id = "..."
twitch_bot_client_secret = "..."
kick_bot_client_id = "..."
kick_bot_client_secret = "..."
trovo_bot_client_id = "..."
trovo_bot_client_secret = "..."
youtube_bot_client_id = "..."
youtube_bot_client_secret = "..."
# AES-256-GCM key used to encrypt OAuth tokens and credentials at rest.
# Generate with: openssl rand -base64 32
token_encryption_key = "..."
Every one of these keys can also be supplied as an environment variable, which wins over the file: LUMIO__AUTH__TWITCH_CLIENT_ID, LUMIO__AUTH__TWITCH_CHANNEL_CLIENT_ID, LUMIO__AUTH__TWITCH_BOT_CLIENT_ID, LUMIO__AUTH__DISCORD_CLIENT_ID, LUMIO__AUTH__TOKEN_ENCRYPTION_KEY, and so on.
The credentials in apps/api/config/local.toml must be identical to those in apps/id/.env.local. The API uses them to refresh login tokens — if they don't match, token refresh will fail silently and users will be logged out.
[auth] carries a Discord entry — discord_client_id — but no discord_client_secret: Discord login tokens are not refreshed by the API, so no secret is needed. The client id is required for token-exchange audience validation (below).
On every token exchange (POST /v1/auth/token, POST /v1/auth/authorize, GraphQL exchangeToken, POST /v1/auth/link) the API validates both the token's subject (the claimed user) and its audience — the OAuth client the token was minted for. Twitch's client_id, Google's aud, and Discord's application.id must each match the configured twitch_client_id / google_client_id / discord_client_id.
This blocks a confused-deputy attack where an attacker registers their own OAuth app, gets a victim to authorize it, and replays the victim-subject token against Lumio (ZAF-1015). The check is fail-closed: if a provider's client id is unset, token exchange for that provider fails (HTTP 500 / INTERNAL_SERVER_ERROR) rather than skipping the check. A foreign-audience token is rejected as an invalid credential (HTTP 401, audited as user:login_failed), identically to a subject mismatch. Set all three client ids in any deployment that serves logins for that provider.
System app environment variables
| Config key | Environment variable |
|---|---|
twitch_channel_client_id | LUMIO__AUTH__TWITCH_CHANNEL_CLIENT_ID |
twitch_channel_client_secret | LUMIO__AUTH__TWITCH_CHANNEL_CLIENT_SECRET |
kick_channel_client_id | LUMIO__AUTH__KICK_CHANNEL_CLIENT_ID |
kick_channel_client_secret | LUMIO__AUTH__KICK_CHANNEL_CLIENT_SECRET |
trovo_channel_client_id | LUMIO__AUTH__TROVO_CHANNEL_CLIENT_ID |
trovo_channel_client_secret | LUMIO__AUTH__TROVO_CHANNEL_CLIENT_SECRET |
twitch_bot_client_id | LUMIO__AUTH__TWITCH_BOT_CLIENT_ID |
twitch_bot_client_secret | LUMIO__AUTH__TWITCH_BOT_CLIENT_SECRET |
kick_bot_client_id | LUMIO__AUTH__KICK_BOT_CLIENT_ID |
kick_bot_client_secret | LUMIO__AUTH__KICK_BOT_CLIENT_SECRET |
trovo_bot_client_id | LUMIO__AUTH__TROVO_BOT_CLIENT_ID |
trovo_bot_client_secret | LUMIO__AUTH__TROVO_BOT_CLIENT_SECRET |
youtube_bot_client_id | LUMIO__AUTH__YOUTUBE_BOT_CLIENT_ID |
youtube_bot_client_secret | LUMIO__AUTH__YOUTUBE_BOT_CLIENT_SECRET |
3. Account Credentials (Database)
Account channel/bot credentials are not configured in files. Each user enters their own Client ID + Secret via Dashboard → Connections → App Credentials. They are stored AES-256-GCM encrypted in the app_credentials table, one row per (account, platform). The resulting access/refresh tokens land in channel_connections or bot_connections, also encrypted.
Bot Connections
Bot connections let Lumio post to chat with a bot identity instead of the channel identity. The OAuth bot platforms are Twitch, YouTube, Kick, and Trovo; Spotify has no bot connection and Discord uses a static admin bot token.
OAuth bot connections default to the system credential source and use dedicated bot app keys from [auth]: twitch_bot_client_id, kick_bot_client_id, trovo_bot_client_id, or youtube_bot_client_id plus the matching secret. Register the unified web-host callback for system bot flows:
{web.public_url}/api/connections/oauth/{platform}/callback
The admin global-bot flow starts in Admin → Providers, returns through the web host, and redirects back to the admin Providers page through the state handle's return_to. Discord global bot setup is not OAuth: paste the Discord bot token in Admin → Providers.
When a deployment has no global key pair for a bot platform, the account bot authorize path degrades to the legacy account source and uses:
{WEB_URL}/api/connections/bot/{platform}/callback
| Platform | Bot 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 | no OAuth — the Discord bot uses a static bot token |
If a platform's dedicated bot keys are unset but the channel keys exist, the bot flow uses the channel keys and logs a warning. This keeps local and operator-managed installs working while making the missing bot app visible. If both bot and channel keys are missing, the flow falls back to account, which is the normal local development path.
Kick production verification now applies to both production Kick apps. The Bot app is the one whose chat.message.sent subscription cap matters for chat sending: unverified apps are capped at 1,000 subscriptions for that event type, verified apps at 10,000.
See Bot Connections for the full flow.
See Also
- OAuth Credentials Architecture — Dual-credential design, encryption, token refresh
- Channel Connections & Scopes — Scope details, connection flow, storage
- Token Refresh — Automatic token refresh worker