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. Includesgenerate_overlay_key()which creates a 12-character URL-safe random key. - Popout Access -- Overlays are accessed via
/overlay/[key]?token=xxxusing popout tokens for non-expiring browser source access.
Frontend
- Overlay editor UI in the Next.js web app.
- Popout overlay renderer loads via the unique overlay key.
- Layer configuration per layer type (alert config, chat style, music widget config, custom HTML/CSS/JS).
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
overlays | overlays:read | List all overlays for the account |
overlay(id: UUID) | overlays:read | Get a single overlay by ID |
overlayLayers(overlayId: UUID) | overlays:read | Get all layers for an overlay, ordered by sort_order |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
createOverlay(input: CreateOverlayInput) | overlays:create | Create a new overlay (defaults: 1920x1080, "transparent" background, "Main Overlay" name) |
updateOverlay(input: UpdateOverlayInput) | overlays:edit | Update overlay name, dimensions, or background |
updateOverlayLayers(overlayId: UUID, layers: JSON) | overlays:edit | Bulk-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) | overlays:delete | Delete an overlay and all its layers |
REST Endpoints
All paths live under /v1/overlays. Bodies are snake_case and mirror the GraphQL inputs.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/overlays | overlays:read | List overlays for the account |
POST | /v1/overlays | overlays:create | Create an overlay |
GET | /v1/overlays/{id} | overlays:read | Get an overlay with its layers |
PATCH | /v1/overlays/{id} | overlays:edit | Update name, dimensions, background, or layers |
DELETE | /v1/overlays/{id} | overlays:delete | Delete an overlay and its layers |
Input Types
CreateOverlayInput:
| Field | Type | Default | Description |
|---|---|---|---|
name | String? | "Main Overlay" | Overlay name |
width | i32? | 1920 | Canvas width in pixels |
height | i32? | 1080 | Canvas height in pixels |
background | String? | "transparent" | Background color/value |
UpdateOverlayInput:
| Field | Type | Description |
|---|---|---|
id | UUID | Overlay ID (required) |
name | String? | New name |
width | i32? | New width |
height | i32? | New height |
background | String? | New background |
Permissions
| Permission | Description |
|---|---|
overlays:read | View overlay configuration and layers |
overlays:create | Create new overlays |
overlays:edit | Edit overlay settings and layers |
overlays:delete | Delete overlays |
Database
| Table | Database | Description |
|---|---|---|
overlays | PostgreSQL | id, account_id, name, key (unique 12-char URL-safe string), width, height, background, created_at, updated_at |
overlay_layers | PostgreSQL | id, overlay_id (FK), type (layer type string), name, config (JSONB), visible (bool), sort_order (int), created_at, updated_at |
overlay_access_tokens | PostgreSQL | id, 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; a validlm_overlay_*token is required. - The server re-validates the token against the database every 30 seconds (heartbeat re-validation). Revoked or rotated tokens cause an immediate disconnect with
TOKEN_REVOKED. - The token carries no RBAC permissions and cannot access REST or GraphQL endpoints.
Token Lifecycle
| Event | Behavior |
|---|---|
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 deletion | The 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
- Open the overlay editor in the Lumio dashboard.
- Click Copy URL — this generates a fresh token and copies the full URL to the clipboard.
- In OBS, add a Browser Source and paste the URL.
- The browser source connects automatically; no further configuration is needed.
Migration for Existing Overlays
Overlays created before the token system do not have a token. Their old URLs no longer grant WebSocket access. To restore connectivity, open the dashboard and click Copy URL to generate a token-bearing URL.
Data Flow
- User creates an overlay via the editor. A unique key and access token are generated.
- User adds layers (alert, chat, music, custom) and configures each one.
- Layers are saved via
updateOverlayLayersmutation (bulk replace). - The overlay is accessed in OBS via its URL containing the overlay key and access token.
- 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!)— requiresoverlays:createpermission andfeature:overlay_foldersfeature flag - REST:
POST /v1/overlay-folders— body:{ "name": "..." }
Moving Overlays
- GraphQL:
moveOverlay(overlayId: UUID!, folderId: UUID)— passnullforfolderIdto 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
| Operation | GraphQL | REST | Permission |
|---|---|---|---|
| List | overlayFolders | GET /v1/overlay-folders | overlays:read |
| Create | createOverlayFolder | POST /v1/overlay-folders | overlays:create |
| Rename | renameOverlayFolder | PATCH /v1/overlay-folders/{id} | overlays:edit |
| Delete | deleteOverlayFolder | DELETE /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:manage-access (Owner and Administrator roles by default) bypass all per-overlay restrictions.
Access Levels
| Level | Can View | Can Edit |
|---|---|---|
| Viewer | Yes | No |
| Editor | Yes | Yes |
| No Access | No | No |
Resolution Logic
- If user has
overlays:manage-accesspermission → full access (bypass) - If user lacks
overlays:read→ no access - If no per-overlay entry exists → default Viewer access
- If entry has role
none→ blocked - If entry has role
editor→ full access - If entry has role
viewer→ view only
Endpoints
| Operation | GraphQL | REST | Permission |
|---|---|---|---|
| List access | overlayAccess(overlayId) | GET /v1/overlays/{id}/access | overlays:manage-access |
| Set access | setOverlayAccess(overlayId, userId, role) | PUT /v1/overlays/{id}/access/{userId} | overlays:manage-access |
| Remove access | removeOverlayAccess(overlayId, userId) | DELETE /v1/overlays/{id}/access/{userId} | overlays:manage-access |
| List candidates | overlayAccessCandidates(overlayId) | GET /v1/overlays/{id}/access/candidates | overlays:manage-access |
All access control endpoints require feature:overlay_access to be enabled.
Shared Links
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:
| Duration | Seconds |
|---|---|
| 1 hour | 3600 |
| 3 hours | 10800 |
| 6 hours | 21600 |
| 12 hours | 43200 |
| 24 hours | 86400 |
Endpoints
| Operation | GraphQL | REST | Permission |
|---|---|---|---|
| List | overlaySharedLinks(overlayId) | GET /v1/overlays/{id}/shared-links | overlays:edit + editor |
| Create | createOverlaySharedLink(overlayId, durationSecs) | POST /v1/overlays/{id}/shared-links | overlays:edit + editor |
| Extend | extendOverlaySharedLink(linkId, durationSecs) | PATCH /v1/overlay-shared-links/{id} | overlays:edit + editor |
| Revoke | revokeOverlaySharedLink(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
| Path | Description |
|---|---|
apps/api/src/graphql/overlays.rs | GraphQL queries and mutations |
apps/api/src/db/overlays.rs | Database operations and key generation |