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).
  • 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 movecopy-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. Revoked or rotated tokens cause an immediate disconnect with TOKEN_REVOKED.
  • The re-validation query has a 500 ms timeout and fails open on timeout: a slow database keeps existing browser sources connected rather than dropping every overlay at once. Revocation therefore takes effect on the next successful check.
  • 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 — 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 token re-validation (TOKEN_REVOKED / TOKEN_EXPIRED)
apps/web/src/app/(overlay)/overlay/[key]/Browser-source renderer