Connections
Overview
The Connections module manages platform OAuth connections for Lumio accounts. It handles two layers of credentials: app credentials (client_id/client_secret for the user's own Twitch/YouTube/Spotify/etc. app) and channel connections (OAuth access/refresh tokens for the specific channel). The module supports the full OAuth flow with PKCE, CSRF protection via Redis-stored state parameters, automatic token refresh, and encrypted credential storage. All secrets are encrypted at rest using AES-256 derived from a master key.
Login Connection Assignments
Login connections (Twitch, YouTube, etc.) are managed at the user level. Each user can have multiple login connections per platform. When a user is a member of multiple Lumio accounts, they can assign which login connection to use on each account.
This allows scenarios like:
- A creator with separate streaming channels assigns different Twitch accounts to different Lumio accounts
- A moderator assigns their personal Twitch login to the accounts they moderate on
Assignments are managed from the Profile page (user-centric view) or the Account Management page (account-centric view).
Database
Assignments are stored in the account_login_assignments table, which maps a login connection to a specific account for a specific user:
| Column | Type | Description |
|---|---|---|
account_id | UUID (FK) | Account the connection is assigned to |
user_id | UUID (FK) | Owner of the login connection |
login_connection_id | UUID (FK) | The login connection being assigned |
provider | TEXT | Platform provider (e.g. twitch, google) |
Permissions
| Permission | Description |
|---|---|
login-assignments:read | View login connection assignments for all users on the account. Own assignments are always visible without this permission. |
login-assignments:create | Assign a login connection on behalf of another user. Own assignments are always allowed. |
login-assignments:delete | Remove a login assignment on behalf of another user. Own assignments are always removable. |
Restrictions
A login connection that is set as the primary connection for an account cannot be disconnected or unassigned. The user must first change or clear the primary connection before removing the assignment.
Reconnect Flags
All three connection tables (channel_connections, login_connections, and bot_connections) have a reconnect_required boolean column (default false).
When reconnect_required is set to true on a connection:
- The Token Refresh Worker skips the connection entirely during its refresh cycle.
- All token retrieval functions (
get_fresh_connection_token,get_fresh_bot_token,get_fresh_oauth_token) return aRowNotFounderror for the flagged connection, causing platform workers that depend on it to stop gracefully. - Users see a "Reconnect Required" banner in the dashboard with a direct reconnect button.
The flag is automatically cleared when the user reconnects through any of the standard flows:
- Login callback (re-authentication clears the flag on the login connection)
- Channel OAuth upsert (reconnecting the channel clears the flag)
- Bot OAuth upsert (reconnecting the bot clears the flag)
Admins can set or clear the flag manually via the admin panel for any connection type. This is useful for forcing a user to re-authorize after a token leak, scope change, or platform-side revocation.
Admin API
| Method | Path | Description |
|---|---|---|
PUT | /v1/admin/channel-connections/\{id\}/reconnect-flag | Set/clear reconnect flag on a channel connection |
PUT | /v1/admin/bot-connections/\{id\}/reconnect-flag | Set/clear reconnect flag on a bot connection |
PUT | /v1/admin/login-connections/\{id\}/reconnect-flag | Set/clear reconnect flag on a login connection |
Request body: { "reconnect_required": true } or { "reconnect_required": false }.
Architecture
Backend
- GraphQL (
apps/api/src/graphql/connections.rs) -- Queries for listing credentials, channel connections, and connection status. Mutations for saving/deleting credentials, initiating OAuth authorization, and disconnecting channels. - REST (
apps/api/src/routes/connections.rs) -- REST endpoints for credential CRUD, channel connection listing, OAuth authorization initiation, OAuth callback handling, and code exchange. REST routes provide the full OAuth flow with redirect handling. - Crypto (
apps/api/src/crypto.rs) -- AES-256 encryption/decryption for client_id, client_secret, access_token, and refresh_token. Key derived fromconfig.auth.token_encryption_key. - Platforms (
apps/api/src/platforms.rs) -- Platform registry with OAuth configuration (authorize URL, token URL, scopes, extra params) per platform.SUPPORTED_PLATFORMSconstant andis_valid_platform()validation.
Frontend
- Connection settings page with cards per platform showing connection status.
- App credential form for entering client_id/client_secret.
- "Connect" button initiates OAuth flow, redirects to platform, callback saves tokens.
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
appCredentials | connections:read | List app credentials for the account. Secrets are masked; only last 4 chars of client_id shown as hint. |
channelConnections | connections:read | List channel connections for the account. Tokens are never exposed. Shows platform, channel ID, channel name, scopes, expiry. |
connectionStatuses | connections:read | Connection status overview for all supported platforms (has_credentials, is_connected, channel_name, enabled) |
enabledProviders(connectionType) | Public (no auth) | List enabled provider slugs for a given connection type (login, channel, or bot) |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
saveAppCredentials(input: SaveCredentialsInput) | connections:create | Save (upsert) app credentials for a platform. Encrypts client_id and client_secret before storage. |
deleteAppCredentials(platform) | connections:delete | Delete app credentials and disconnect the channel connection |
authorizeChannel(platform) | connections:create | Start OAuth authorization flow. Generates a state parameter (stored in Redis with 10-min TTL), builds the authorization URL with scopes, and returns the redirect URL. |
disconnectChannel(platform) | connections:delete | Disconnect a channel connection (keeps app credentials) |
REST Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/connections/credentials | connections:read | List all app credentials |
PUT | /v1/connections/credentials/{platform} | connections:create | Save app credentials |
DELETE | /v1/connections/credentials/{platform} | connections:delete | Delete app credentials + stop platform worker |
GET | /v1/connections/channel | connections:read | List all channel connections |
DELETE | /v1/connections/channel/{platform} | connections:delete | Disconnect a channel |
GET | /v1/connections/channel/{platform}/authorize | connections:create | Initiate OAuth flow (redirect URL) |
GET | /v1/connections/channel/{platform}/callback | -- | OAuth callback handler |
POST | /v1/connections/channel/{platform}/exchange | -- | Exchange OAuth code for tokens |
Permissions
| Permission | Description |
|---|---|
connections:read | View credentials (masked) and channel connections |
connections:create | Save app credentials, initiate OAuth flows |
connections:edit | Edit connection settings |
connections:delete | Delete credentials, disconnect channels |
Database
| Table | Database | Description |
|---|---|---|
app_credentials | PostgreSQL | id, account_id, platform, client_id (encrypted), client_secret (encrypted), created_at, updated_at. Unique on (account_id, platform). |
channel_connections | PostgreSQL | id, account_id, platform, platform_channel_id, channel_name, access_token (encrypted), refresh_token (encrypted), scopes (text array), reconnect_required (bool, default false), expires_at, created_at, updated_at. Unique on (account_id, platform). |
Data Flow
OAuth Connection Flow
- User enters app credentials (client_id/client_secret) for a platform.
- Credentials are encrypted with AES-256 and stored in
app_credentials. - User clicks "Connect" which calls
authorizeChannel(platform). - Server generates a CSRF state parameter (
account_id:uuid), stores it in Redis with 10-minute TTL. - Server builds the OAuth authorization URL with the platform's authorize endpoint, scopes, redirect URI, and state.
- Client redirects to the platform's authorization page.
- User authorizes on the platform. Platform redirects back to the callback URL with
codeandstate. - Callback handler validates the state against Redis, exchanges the code for tokens via the platform's token endpoint.
- Access token and refresh token are encrypted and stored in
channel_connections. - Platform worker is started for the new connection.
Security
- Encryption at rest: All client_id, client_secret, access_token, and refresh_token values are encrypted using AES-256 with a key derived from
config.auth.token_encryption_key. - CSRF protection: OAuth state parameter stored in Redis with 10-minute TTL.
- Secret masking: Client secrets and tokens are never exposed in API responses. Only a 4-character hint of the client_id is shown.
- Spotify localhost workaround: For development, Spotify requires
127.0.0.1instead oflocalhostin redirect URIs.
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/connections.rs | GraphQL queries and mutations |
apps/api/src/routes/connections.rs | REST endpoints including OAuth callback/exchange |
apps/api/src/crypto.rs | AES-256 encryption/decryption utilities |
apps/api/src/platforms.rs | Platform OAuth configuration registry |
apps/api/src/db/connections.rs | Database operations for credentials and connections |