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
- Create -- User provides their StreamElements JWT token and platform. The token is encrypted with AES using a derived key from
auth.token_encryption_keyand stored via upsert (one token per account+platform). - Worker Start -- The REST layer starts a
StreamElementsConfigworker viaWorkerManager, passing the unencrypted token and itsse_tokensrow 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. - List -- Returns tokens with masked hints (first 4 + last 4 chars, middle replaced with
***). The actual token value is never returned. - 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. - Boot Recovery --
list_all_accounts_with_tokensis 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
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/se-tokens | List all SE tokens (masked) | se-tokens:read + feature:streamelements |
POST | /v1/se-tokens | Create/update an SE token | se-tokens:create + feature:streamelements |
DELETE | /v1/se-tokens/{id} | Delete an SE token | se-tokens:delete + feature:streamelements |
Admin-panel counterparts (admin-scoped permissions, checked with
require_permission against the admin role set):
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/admin/accounts/{id}/se-tokens | List any account's SE tokens (masked) | se-tokens:read |
DELETE | /v1/admin/accounts/{id}/se-tokens/{token_id} | Delete an SE token on any account | se-tokens:delete |
POST /v1/se-tokens
{
"platform": "twitch",
"token": "eyJhbGciOiJI...",
"label": "My SE Token"
}
Validation:
platformmust be one of:twitch,youtube,kick,trovotokenmust not be empty
Response (201): Returns the created token with masked hint and starts the SE worker.
GraphQL Queries
| Query | Args | Returns | Permission |
|---|---|---|---|
seTokens | -- | [SeToken!]! | se-tokens:read + feature:streamelements |
adminSeTokens | accountId: UUID! | [AdminSeToken!]! | admin se-tokens:read |
GraphQL Mutations
| Mutation | Args | Returns | Permission |
|---|---|---|---|
createSeToken | platform: String!, token: String!, label: String | SeToken! | se-tokens:create + feature:streamelements |
deleteSeToken | id: UUID! | Boolean! | se-tokens:delete + feature:streamelements |
adminDeleteSeToken | accountId: 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
| Permission | Description |
|---|---|
se-tokens:read | List SE tokens |
se-tokens:create | Create/update SE tokens |
se-tokens:delete | Delete 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
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Token ID |
account_id | UUID (FK) | Owning account |
platform | TEXT | Platform identifier |
token | TEXT | Encrypted JWT token |
label | TEXT | Optional user label |
created_at | TIMESTAMPTZ | Creation timestamp |
updated_at | TIMESTAMPTZ | Last update timestamp |
Unique constraint: (account_id, platform) -- one token per platform per account.
DB Functions
| Function | Description |
|---|---|
list_tokens | List all tokens for an account, ordered by platform |
upsert_token | Insert or update on (account_id, platform) conflict |
delete_token | Delete by ID with account ownership check |
has_tokens | Check if an account has any tokens |
list_all_accounts_with_tokens | Boot-time recovery: all accounts grouped with their tokens |
Key Files
| File | Purpose |
|---|---|
apps/api/src/graphql/se_tokens.rs | GraphQL queries and mutations |
apps/api/src/routes/se_tokens.rs | REST endpoints with worker lifecycle management |
apps/api/src/db/se_tokens.rs | Database CRUD operations |
crates/lo-auth/src/rbac.rs | Permission constants (se-tokens:read/create/delete) |