Skip to main content

StreamElements Tokens

Overview

The SE Tokens module manages StreamElements JWT tokens used for receiving tip/donation events via the StreamElements WebSocket API. Tokens are stored encrypted at rest and scoped per account+platform. Each token runs its own StreamElements WebSocket worker (keyed by token id), so an account can receive tips from several platforms at once. Creating or re-saving a token replaces that token's worker; deleting a token stops only that token's worker and leaves the account's other SE workers running.

Supported platforms: Twitch, YouTube, Kick, Trovo.

Architecture

Dashboard UI
|
v
Next.js API Proxy (/api/se-tokens)
|
+---> REST API (GET/POST/DELETE /v1/se-tokens)
| |
| +--> WorkerManager (start/stop SE WebSocket worker)
|
+---> GraphQL (SeTokenQuery / SeTokenMutation)
|
v
db::se_tokens (PostgreSQL, encrypted token storage)

Token Lifecycle

  1. Create -- User provides their StreamElements JWT token and platform. The token is encrypted with AES using a derived key from auth.token_encryption_key and stored via upsert (one token per account+platform).
  2. Worker Start -- The REST layer starts a StreamElementsConfig worker via WorkerManager, passing the unencrypted token and its se_tokens row id for the WebSocket connection. Workers are keyed by token id, so re-saving a token replaces its worker instead of spawning a duplicate, and workers for the account's other platforms are untouched.
  3. List -- Returns tokens with masked hints (first 4 + last 4 chars, middle replaced with ***). The actual token value is never returned.
  4. Delete -- Removes the token and stops that token's worker via WorkerManager.stop_se_worker (addressed by token id). Other SE workers for the account keep running.
  5. Boot Recovery -- list_all_accounts_with_tokens is used at server startup to auto-start SE workers for all accounts with stored tokens.

Token Masking

Tokens returned to clients show only a hint: eyJh***ab12. If the token is 8 characters or shorter, or decryption fails, **** is returned instead.

API

REST Endpoints

MethodPathDescriptionPermission
GET/v1/se-tokensList all SE tokens (masked)se-tokens:read + feature:streamelements
POST/v1/se-tokensCreate/update an SE tokense-tokens:create + feature:streamelements
DELETE/v1/se-tokens/{id}Delete an SE tokense-tokens:delete + feature:streamelements

Admin-panel counterparts (admin-scoped permissions, checked with require_permission against the admin role set):

MethodPathDescriptionPermission
GET/v1/admin/accounts/{id}/se-tokensList any account's SE tokens (masked)se-tokens:read
DELETE/v1/admin/accounts/{id}/se-tokens/{token_id}Delete an SE token on any accountse-tokens:delete

POST /v1/se-tokens

{
"platform": "twitch",
"token": "eyJhbGciOiJI...",
"label": "My SE Token"
}

Validation:

  • platform must be one of: twitch, youtube, kick, trovo
  • token must not be empty

Response (201): Returns the created token with masked hint and starts the SE worker.

GraphQL Queries

QueryArgsReturnsPermission
seTokens--[SeToken!]!se-tokens:read + feature:streamelements
adminSeTokensaccountId: UUID![AdminSeToken!]!admin se-tokens:read

GraphQL Mutations

MutationArgsReturnsPermission
createSeTokenplatform: String!, token: String!, label: StringSeToken!se-tokens:create + feature:streamelements
deleteSeTokenid: UUID!Boolean!se-tokens:delete + feature:streamelements
adminDeleteSeTokenaccountId: UUID!, tokenId: UUID!Boolean!admin se-tokens:delete

Both protocols compose the same two gates: the feature:streamelements plan flag and the RBAC permission (FeatureGuard::new("feature:streamelements").and(PermissionGuard::new(…)) on GraphQL; auth.require_permission(…) + require_feature(…) on REST). An account whose plan does not carry feature:streamelements cannot reach any of these operations. The admin twins use AdminPermissionGuard / the admin require_permission and carry no feature gate — they act across accounts.

Note: The GraphQL layer does not have access to WorkerManager, so worker lifecycle is handled exclusively by the REST endpoints. createSeToken and deleteSeToken on GraphQL write the encrypted row but do not start or stop the StreamElements WebSocket worker; use the REST endpoints when the worker must follow the change immediately. Boot recovery picks up GraphQL-created tokens on the next API start.

GraphQL Types

type SeToken {
id: UUID!
platform: String!
tokenHint: String!
label: String
createdAt: String!
updatedAt: String!
}

"""SE token (masked) in admin view — no updatedAt."""
type AdminSeToken {
id: UUID!
platform: String!
tokenHint: String!
label: String
createdAt: String!
}

WebSocket

SE tokens have no channel of their own. The tips/donations they unlock arrive on the account's events:{account_id} stream (events:read, see crates/lo-websocket/src/gate.rs) as normal platform events.

Permissions

PermissionDescription
se-tokens:readList SE tokens
se-tokens:createCreate/update SE tokens
se-tokens:deleteDelete SE tokens

Included in: Owner, Administrator roles. Moderator and Viewer receive none of the three.

The same three keys double as admin permissions for the cross-account admin surface (adminSeTokens / adminDeleteSeToken and their REST twins), where they are resolved against the caller's admin roles rather than their account roles.

Feature flag

Every account-scoped SE-token operation additionally requires the feature:streamelements flag on the account's plan (category feature, label "StreamElements"). Without it both protocols answer FEATURE_DISABLED.

Database

Table: se_tokens

ColumnTypeDescription
idUUID (PK)Token ID
account_idUUID (FK)Owning account
platformTEXTPlatform identifier
tokenTEXTEncrypted JWT token
labelTEXTOptional user label
created_atTIMESTAMPTZCreation timestamp
updated_atTIMESTAMPTZLast update timestamp

Unique constraint: (account_id, platform) -- one token per platform per account.

DB Functions

FunctionDescription
list_tokensList all tokens for an account, ordered by platform
upsert_tokenInsert or update on (account_id, platform) conflict
delete_tokenDelete by ID with account ownership check
has_tokensCheck if an account has any tokens
list_all_accounts_with_tokensBoot-time recovery: all accounts grouped with their tokens

Key Files

FilePurpose
apps/api/src/graphql/se_tokens.rsGraphQL queries and mutations
apps/api/src/routes/se_tokens.rsREST endpoints with worker lifecycle management
apps/api/src/db/se_tokens.rsDatabase CRUD operations
crates/lo-auth/src/rbac.rsPermission constants (se-tokens:read/create/delete)