Channel Connections & OAuth Scopes
Channel connections are the mechanism by which Lumio accesses platform APIs on behalf of a user's channel. Each connection stores encrypted OAuth tokens and the scopes that were granted during authorization.
One Connection Per Platform
Each account can have one channel connection per platform. This is enforced by a UNIQUE (account_id, platform) constraint on the channel_connections table.
Supported platforms: Twitch, YouTube, Kick, Trovo, Spotify.
OAuth Scopes by Platform
Scopes are defined statically in apps/api/src/platforms.rs (get_oauth_config()). They are requested during the authorization flow and cannot be changed after the connection is established. To change scopes, the user must disconnect and reconnect.
Twitch (27 scopes)
| 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 (used by ProfileService) |
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 (delete, timeout) |
moderator:read:blocked_terms | Read blocked terms list |
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 |
Extra params: force_verify=true (forces re-authorization even if already granted)
YouTube (2 scopes)
| Scope | Purpose |
|---|---|
youtube.readonly | Read channel, video, and live chat data |
youtube.force-ssl | Full YouTube Data API access (chat, comments) |
Extra params: access_type=offline (enables refresh tokens), prompt=consent (forces consent screen)
Kick (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 |
Trovo (2 scopes)
| Scope | Purpose |
|---|---|
channel_details_self | Read own channel details |
channel_subscriptions | Read subscriber data |
Spotify (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 |
OAuth Connection Flow
The complete flow from credentials to active worker:
sequenceDiagram
participant User
participant Dashboard as Dashboard (Next.js)
participant API as API Server (Rust)
participant DB as PostgreSQL
participant Platform as Platform OAuth
Note over User,Dashboard: Step 1: Save App Credentials
User->>Dashboard: Enter client_id + client_secret
Dashboard->>API: saveAppCredentials mutation
API->>API: Encrypt with AES-256-GCM
API->>DB: Upsert into app_credentials
API-->>Dashboard: Return client_id_hint (last 4 chars)
Note over User,Platform: Step 2: Authorize Channel
User->>Dashboard: Click "Connect"
Dashboard->>API: Get authorize URL
API->>API: Build URL with scopes from get_oauth_config()
API-->>Dashboard: Redirect URL
Dashboard->>Platform: Redirect to OAuth authorize
Platform->>User: Show consent screen
User->>Platform: Approve
Platform-->>API: Callback with authorization code
Note over API,DB: Step 3: Token Exchange
API->>API: Decrypt client_id + client_secret from app_credentials
API->>Platform: Exchange code for access_token + refresh_token
Platform-->>API: Token response
API->>Platform: Fetch channel info (name, ID)
Platform-->>API: Channel details
API->>API: Encrypt tokens with AES-256-GCM
API->>DB: Upsert into channel_connections
Note over API: Step 4: Start Worker
API->>API: Start platform worker (EventSub, chat listener, etc.)
Scope Behavior
Static Scopes
Scopes are defined at compile time in get_oauth_config(). When the authorize URL is built, all scopes for the platform are requested. There is no mechanism for requesting partial scopes.
Changing Scopes
If the required scopes change (e.g., after a Lumio update adds new features), users must:
- Disconnect the existing channel connection
- Reconnect to trigger a new OAuth flow with the updated scope list
The old tokens continue to work for their originally granted scopes, but new features requiring additional scopes will fail until reconnection.
Scope Storage
Granted scopes are stored in the scopes column of channel_connections as a VARCHAR[] array. This records what was actually granted (which may differ from what was requested if the user denied specific scopes on platforms that support partial grants).
Credential Requirements
Both tables are required for a fully functional connection:
| Table | Required For |
|---|---|
app_credentials | Token refresh, API client headers (client_id in requests) |
channel_connections | All API calls (access_token as bearer token) |
If app_credentials are deleted, the connection's access_token continues to work until it expires, but refresh will fail.
If channel_connections are deleted, no API calls can be made for that platform.
Deleting app_credentials via the API also deletes the associated channel_connections entry.
Key Files
| File | Purpose |
|---|---|
apps/api/src/platforms.rs | get_oauth_config() -- scopes, URLs, extra params per platform |
apps/api/src/db/connections.rs | CRUD for app_credentials and channel_connections |
apps/api/src/graphql/connections.rs | GraphQL mutations for save/delete credentials, disconnect |
apps/api/src/oauth.rs | OAuthTokens -- token refresh logic |
apps/api/src/crypto.rs | AES-256-GCM encryption/decryption |