Skip to main content

Overlays

Overview​

The Overlays module provides a custom streaming overlay editor where users can create, configure, and manage overlays with layered widgets. Each overlay has a unique key used for popout access in browser sources. Overlays support configurable dimensions, background settings, and multiple layer types (alerts, chat, music, custom HTML/CSS). Layers are ordered by sort_order and can be individually toggled visible/hidden.

Architecture​

Backend​

  • GraphQL (apps/api/src/graphql/overlays.rs) -- Full CRUD for overlays and layers. Queries for listing/fetching overlays and their layers. Mutations for create, update, delete overlays and bulk-replace layers.
  • Database (apps/api/src/db/overlays.rs) -- PostgreSQL operations for overlays and overlay_layers tables. Includes generate_overlay_key() which creates a 12-character URL-safe random key.
  • Browser Source Access -- Overlays render at /overlay/[key]?token=…. The token is the overlay's own Overlay Access Token (lm_overlay_*) or a temporary Shared Overlay Token (lm_share_*) - not a popout token. The page returns 403 - Access token required when the query parameter is absent.

Frontend​

  • Overlay editor UI in the Next.js web app under /dashboard/overlays.
  • The browser-source renderer (apps/web/src/app/(overlay)/overlay/[key]/) loads via the unique overlay key and passes the access token straight into the WebSocket URL.
  • Layer configuration per layer type (alert config, chat style, music widget config, custom HTML/CSS/JS).
  • Extension layers (type: "ext:{slug}") mount the installed extension inside the layer's box when the overlay's account has the extension installed and enabled. The extension runs with the overlay's own token, so it can use useLumioAction() for every action except chat actions (chat:*), which an overlay link may never trigger. Without an enabled install the layer stays empty on stream.
  • OBS connection parameters can be supplied to a shared link through obs_host / obs_port / obs_password query parameters; for a permanent token they come from the overlay's stored bootstrap configuration instead.

Token Types on Overlay Surfaces​

Three different tokens appear around overlays; they are not interchangeable:

TokenPrefixUsed for
Overlay Access Tokenlm_overlay_*The overlay's own permanent Browser Source URL (/overlay/[key])
Shared Overlay Tokenlm_share_*Time-limited shared access to one overlay
Popout Tokenlm_pop_*Dashboard popout views (/popout/[view]) - carries RBAC permissions, unlike the two above

Dashboard popout views and the API hooks behind them authenticate with a popout token rather than a session cookie, so every hook they use must accept popout-token auth as well as cookie auth. The overlay browser source is the opposite case: its token carries no RBAC permissions and can reach only the overlay's WebSocket channel.

The permanent Overlay Access Token (and the equivalent Widget Access Token) is stored AES-256-GCM-encrypted at rest, the same pattern as app_credentials: auth resolution keys on the token hash, and the encrypted copy exists only so the dashboard can decrypt it on demand to rebuild the Browser Source URL - the cleartext never sits in the database. Resolution is additionally gated on revoked_at / expires_at on every request, like the popout and shared-overlay tokens. Rotating a token (copyOverlayUrl) is a de-facto revocation of the old one.

API​

GraphQL Queries​

Every overlay query and mutation is gated by a feature flag and an RBAC permission, composed as FeatureGuard::new(<flag>).and(PermissionGuard::new(<permission>)) on GraphQL and as auth.require_permission(…) + require_feature(…) on REST. The flag differs by area: core CRUD uses feature:overlays, folders use feature:overlay_folders, per-overlay access control uses feature:overlay_access, and shared links use feature:overlay_sharing.

QueryFeaturePermissionDescription
overlaysfeature:overlaysoverlays:readList all overlays for the account
overlay(id: UUID)feature:overlaysoverlays:readGet a single overlay by ID
overlayLayers(overlayId: UUID)feature:overlaysoverlays:readGet all layers for an overlay, ordered by sortOrder
overlayAccessUrl(overlayId: UUID)feature:overlaysoverlays:read + Editor accessReturn the overlay's Browser Source URL from the stored token, without rotating it. Handing out the cleartext lm_overlay_* token requires Editor-level access to the overlay (mirrors copyOverlayUrl), not merely overlays:read - a read-only Viewer cannot fetch it. null when the overlay has no retrievable token.
overlayFoldersfeature:overlay_foldersoverlays:readList the account's overlay folders
overlayAccess(overlayId)feature:overlay_accessoverlays:access-readList per-overlay access entries
overlayAccessCandidates(overlayId)feature:overlay_accessoverlays:access-readList members who can be granted access
overlaySharedLinks(overlayId)feature:overlay_sharingoverlays:editList the overlay's temporary shared links

GraphQL Mutations​

MutationFeaturePermissionDescription
createOverlay(input: CreateOverlayInput)feature:overlaysoverlays:createCreate a new overlay (defaults: 1920x1080, "transparent" background, "Main Overlay" name)
updateOverlay(input: UpdateOverlayInput)feature:overlaysoverlays:editUpdate overlay name, dimensions, or background
updateOverlayLayers(overlayId: UUID, layers: JSON)feature:overlaysoverlays:editBulk-replace all layers for an overlay. Each layer has: type, name, config (JSONB), visible, sort_order. Optionally include id to preserve identity.
deleteOverlay(id: UUID)feature:overlaysoverlays:deleteDelete an overlay and all its layers
copyOverlayUrl(overlayId: UUID)feature:overlaysoverlays:editRotate the overlay access token and return the fresh Browser Source URL. The previous URL stops working immediately.
revokeOverlayToken(overlayId: UUID)feature:overlaysoverlays:editDestroy the overlay access token without re-issuing one
moveOverlay(overlayId: UUID, folderId: UUID)feature:overlay_foldersoverlays:editMove an overlay into a folder; null moves it back to root

REST Endpoints​

Bodies are snake_case and mirror the GraphQL inputs.

MethodPathPermissionDescription
GET/v1/overlaysoverlays:readList overlays for the account
POST/v1/overlaysoverlays:createCreate an overlay
GET/v1/overlays/{id}overlays:readGet an overlay with its layers
PATCH/v1/overlays/{id}overlays:editUpdate name, dimensions, background, or layers
DELETE/v1/overlays/{id}overlays:deleteDelete an overlay and its layers
POST/v1/overlays/{id}/copy-urloverlays:editRotate the access token and return the fresh Browser Source URL
POST/v1/overlays/{id}/revoke-tokenoverlays:editRevoke the access token without re-issuing
POST/v1/overlays/{id}/moveoverlays:editMove the overlay into a folder (feature:overlay_folders)

The same feature flags apply on REST. Note that apps/api/openapi.json currently registers only the core five plus move - copy-url, revoke-token, the access endpoints and the shared-link endpoints are served but are not listed in the generated OpenAPI document.

There is no REST route for reading an overlay's layers on their own or for the stored Browser Source URL; use the overlayLayers and overlayAccessUrl GraphQL queries. The web dashboard reaches both through its Next.js proxy routes (/api/overlays/[id]/layers, /api/overlays/[id]/url), which execute those GraphQL operations server-side.

Input Types​

CreateOverlayInput:

FieldTypeDefaultDescription
nameString?"Main Overlay"Overlay name
widthi32?1920Canvas width in pixels
heighti32?1080Canvas height in pixels
backgroundString?"transparent"Background color/value

UpdateOverlayInput:

FieldTypeDescription
idUUIDOverlay ID (required)
nameString?New name
widthi32?New width
heighti32?New height
backgroundString?New background

Permissions​

PermissionDescription
overlays:readView overlay configuration and layers
overlays:createCreate new overlays
overlays:editEdit overlay settings and layers
overlays:deleteDelete overlays

Database​

TableDatabaseDescription
overlaysPostgreSQLid, account_id, name, key (unique 12-char URL-safe string), width, height, background, created_at, updated_at
overlay_layersPostgreSQLid, overlay_id (FK), type (layer type string), name, config (JSONB), visible (bool), sort_order (int), created_at, updated_at
overlay_access_tokensPostgreSQLid, overlay_id (FK, unique), account_id (FK), token_hash (SHA-256, unique), created_at. One token per overlay; cascade-deleted with the overlay.

Overlay Key Generation​

The generate_overlay_key() function creates a 12-character random string using characters A-Z, a-z, 0-9, -, _. This key is used in the popout URL: /overlay/[key]?token=xxx.

Access Tokens​

Each overlay is protected by a dedicated access token (lm_overlay_* prefix). The token is a 75-character hex-encoded string stored as a SHA-256 hash in the overlay_access_tokens table. Access requires both the overlay key (in the URL path) and a valid token - a two-secret-layer model that prevents unauthorized subscribes even if one value leaks.

Security Model​

  • Anonymous WebSocket subscribes to overlay:{key} channels are blocked. channel_gate_for("overlay") returns ChannelGate::OverlayToken, which requires an AuthContext::OverlayToken whose overlay_id matches the overlay resolved from the channel key - so one overlay's token cannot subscribe to another's channel.
  • The server re-validates the token against the database every 30 seconds, throttled inside the 5-second heartbeat tick. The check reads the token hash, so a rotated, revoked or deleted token causes a disconnect with TOKEN_REVOKED on the next check (rotation keeps the row id, but the old secret no longer matches). There is no validity cache that could keep a revoked token alive.
  • The re-validation query has a 500 ms timeout and fails closed: one timeout or database error keeps the last (valid) result once, a second one in a row disconnects the browser source with AUTH_UNAVAILABLE, and it reconnects with backoff. See Credential re-validation.
  • The token carries no RBAC permissions and cannot access REST or GraphQL endpoints.
  • The overlay channel type has no entry in channel_feature_for, so the WebSocket layer applies no plan-feature gate to it; the plan check happens when the overlay and its token are managed through GraphQL/REST.

Token Lifecycle​

EventBehavior
Overlay creation (createOverlay)A token is auto-issued and returned in the response
Copy URL (dashboard button)The existing token is revoked and a fresh token is issued (auto-rotation). The old URL stops working immediately.
Revoke (revokeOverlayToken)The token is destroyed without re-issue. Use for compromise emergencies - the overlay becomes inaccessible until a new URL is copied.
Overlay deletionThe token is cascade-deleted with the overlay row

Transport​

The token is passed via ?token=lm_overlay_… query parameter (primary, for browser sources) or Authorization: Bearer lm_overlay_… header.

OBS Setup​

  1. Open the overlay editor in the Lumio dashboard.
  2. Click Copy URL - this generates a fresh token and copies the full URL to the clipboard.
  3. In OBS, add a Browser Source and paste the URL.
  4. The browser source connects automatically; no further configuration is needed.

Overlays Without a Token​

An overlay can exist without an access token - for example after revokeOverlayToken, or for rows that predate the token requirement. overlayAccessUrl returns null for such an overlay and its URL grants no WebSocket access. Clicking Copy URL (copyOverlayUrl) issues a token and produces a working URL.

Data Flow​

  1. User creates an overlay via the editor. A unique key and access token are generated.
  2. User adds layers (alert, chat, music, custom) and configures each one.
  3. Layers are saved via updateOverlayLayers mutation (bulk replace).
  4. The overlay is accessed in OBS via its URL containing the overlay key and access token.
  5. The popout page loads the overlay and all its layers, connecting to WebSocket for real-time updates using the access token for authentication.

Folders​

Overlays can be organized into flat folders (one level, no nesting). Each account has unique folder names.

Creating Folders​

  • GraphQL: createOverlayFolder(name: String!) - requires overlays:create permission and feature:overlay_folders feature flag
  • REST: POST /v1/overlay-folders - body: { "name": "..." }

Moving Overlays​

  • GraphQL: moveOverlay(overlayId: UUID!, folderId: UUID) - pass null for folderId to move back to root
  • REST: POST /v1/overlays/{id}/move - body: { "folder_id": "..." | null }

Requires overlays:edit permission, feature:overlay_folders feature flag, and per-overlay editor access.

Folder Management​

OperationGraphQLRESTPermission
ListoverlayFoldersGET /v1/overlay-foldersoverlays:read
CreatecreateOverlayFolderPOST /v1/overlay-foldersoverlays:create
RenamerenameOverlayFolderPATCH /v1/overlay-folders/{id}overlays:edit
DeletedeleteOverlayFolderDELETE /v1/overlay-folders/{id}overlays:delete

All folder endpoints require feature:overlay_folders to be enabled for the account's plan.

Access Control​

Per-overlay member access control refines the account-level RBAC permissions. Users with overlays:access-read (Owner and Administrator roles by default) bypass all per-overlay restrictions.

Access Levels​

LevelCan ViewCan Edit
ViewerYesNo
EditorYesYes
No AccessNoNo

Resolution Logic​

db::overlay_access::can_access_overlay resolves in this order:

  1. Verify the overlay actually belongs to the caller's account. A mismatch is denied outright - this runs first, so the overlays:access-read bypass can never reach another account's overlay.
  2. If the caller has overlays:access-read → full access (bypass)
  3. If the caller lacks overlays:read → no access
  4. If the auth context has no user_id → no access (a per-overlay entry is keyed on a user)
  5. If no per-overlay entry exists → default Viewer access
  6. If entry has role none → blocked
  7. If entry has role editor → full access
  8. If entry has role viewer → view only

Steps 5–8 are shared with other per-resource access checks via db::resource_access::resolve_access.

Endpoints​

OperationGraphQLRESTPermission
List accessoverlayAccess(overlayId)GET /v1/overlays/{id}/accessoverlays:access-read
Set accesssetOverlayAccess(overlayId, userId, role)PUT /v1/overlays/{id}/access/{userId}overlays:access-grant
Remove accessremoveOverlayAccess(overlayId, userId)DELETE /v1/overlays/{id}/access/{userId}overlays:access-revoke
List candidatesoverlayAccessCandidates(overlayId)GET /v1/overlays/{id}/access/candidatesoverlays:access-read

All access control endpoints require feature:overlay_access to be enabled.

Temporary shared overlay links allow time-limited access to a specific overlay without requiring account membership. Links use the lm_share_ token type (7th authentication type).

Token Format​

  • Prefix: lm_share_ (9 characters)
  • Body: 32 random bytes, hex-encoded (64 characters)
  • Total length: 73 characters
  • Storage: SHA-256 hash in database

Allowed Durations​

Links can be created with one of five fixed durations:

DurationSeconds
1 hour3600
3 hours10800
6 hours21600
12 hours43200
24 hours86400

Endpoints​

OperationGraphQLRESTPermission
ListoverlaySharedLinks(overlayId)GET /v1/overlays/{id}/shared-linksoverlays:edit + editor
CreatecreateOverlaySharedLink(overlayId, durationSecs)POST /v1/overlays/{id}/shared-linksoverlays:edit + editor
ExtendextendOverlaySharedLink(linkId, durationSecs)PATCH /v1/overlay-shared-links/{id}overlays:edit + editor
RevokerevokeOverlaySharedLink(linkId)DELETE /v1/overlay-shared-links/{id}overlays:edit + editor

All shared link endpoints require feature:overlay_sharing to be enabled.

WebSocket Access​

Shared links authenticate via the ?token=lm_share_... query parameter on WebSocket connections. The token grants subscription to the overlay's WebSocket channel (overlay:{key}). The server performs periodic heartbeat checks:

  • In-memory expiry: checked every heartbeat tick (5s) - disconnects with TOKEN_EXPIRED
  • DB revocation: checked every 30 seconds by the credential re-validation - disconnects with TOKEN_REVOKED

Key Files​

PathDescription
apps/api/src/graphql/overlays.rsGraphQL queries and mutations
apps/api/src/routes/overlays.rsREST handlers (/v1/overlays, /v1/overlay-shared-links)
apps/api/src/routes/overlay_folders.rsREST handlers for /v1/overlay-folders
apps/api/src/db/overlays.rsDatabase operations, key generation, access-token issuance
apps/api/src/db/overlay_shared_links.rsShared-link rows and ALLOWED_DURATIONS
crates/lo-auth/src/overlay_token.rslm_overlay_* generation and prefix validation
crates/lo-auth/src/shared_token.rslm_share_* generation and prefix validation
crates/lo-websocket/src/session.rsHeartbeat credential re-validation (TOKEN_REVOKED / TOKEN_EXPIRED / AUTH_UNAVAILABLE)
apps/api/src/db/credential_revalidation.rsPer-token-kind database re-check behind the heartbeat
apps/web/src/app/(overlay)/overlay/[key]/Browser-source renderer