Extensions
Extensions add features to Lumio accounts: widgets for overlays, bot modules for chat moderation, automation nodes, themes, and integrations. They are discovered and installed from the Extension Store.
Visibility levels
| Visibility | Description |
|---|---|
system | Built-in extensions maintained by Lumio. Always available, cannot be uninstalled. |
public | Community extensions published by verified developers. Anyone can install them. |
paid | Community extensions with a price. Purchase required to install. |
unlisted | Not shown in Store search. Installable via direct link. |
private | Hidden from public discovery. A published extension is installable only by an account with an access grant or an invited tester. |
Developers can set private, unlisted, public, or paid. The system value and the system- slug prefix are reserved for Lumio's built-in extensions. Create and update requests reject other visibility values and developer slugs beginning with system- (case-insensitive). A developer- or team-owned extension that already carries system visibility or a system- slug is never treated as a built-in extension. The API logs a warning for each such extension at startup so an operator can review it; the check does not change the extension.
Installation
REST and GraphQL install requests require a published extension. Public, paid, unlisted, and system extensions can pass the visibility check; private extensions additionally require an access grant for the installing account or a tester invitation for the acting user. Payment and account permissions still apply where required. An extension that does not pass this check returns 404 Extension not found. Revoking a private access grant also prevents reinstalling through that grant.
Buying a paid extension (POST /v1/extension-purchases) runs the same check before a Stripe Checkout session is created, and before the free-access shortcut installs. An account that cannot install the extension gets 404 Extension not found and is not sent to payment. When Stripe reports the completed payment, the API checks again before it installs the extension. If the check fails at that point, for example because the access grant was revoked after checkout, the payment is recorded but the extension is not installed. The same applies when the extension or its version is deleted or unpublished while the buyer is still on the Stripe page: the payment is recorded without the deleted extension or version and nothing is installed. In both cases Lumio refunds the full amount automatically, right when the payment is reported; a subscription is cancelled at once, without proration, and its first payment is refunded. The purchase then shows as refunded. Lumio also e-mails the buyer that the extension was no longer available and that the full amount was refunded, because Stripe sends its own receipt only when that is enabled in the Stripe Dashboard. A payment that is not installed because of a temporary error is not refunded automatically. A recorded purchase does not give access to a private extension. A paid checkout also requires the buyer's withdrawal-waiver declaration (accept_withdrawal_waiver: true, text B for one-time purchases, text C for subscriptions) and sends an order confirmation after payment; see Billing → Withdrawal waiver and order confirmation. Purchase is available only over REST: checkout ends in a redirect to Stripe and a signed webhook, so there is no GraphQL mutation for it.
Draft and testing versions can be inspected by their developer, but an extension must have published status before the install endpoint accepts it.
Installing an extension the account already has returns 409 Extension is already installed (GraphQL: Extension is already installed).
Uninstalling and reinstalling
Uninstalling (DELETE /v1/extension-installs/{id}, GraphQL uninstallExtension) takes the extension off the account at once. Its widgets, overlay layers and bundled sounds are deleted. The install no longer appears in install lists, the store's installed badge, or install lookups. Its functions, actions and storage answer Extension install not found, and bots and workers stop running its handlers and triggers.
By default the extension's stored data is kept for 30 days and deleted afterwards; delete_data_immediately deletes it right away. Deleting the data only ever removes this account's data: an extension keeps one database schema for every account that installs it, so uninstalling deletes this account's rows from it, and the schema itself is removed only with the extension's last install. Every uninstall is recorded in the account's audit log as account:extension_uninstalled, including whether the data was deleted immediately. Installing the same extension again within those 30 days restores the install on the latest published version, with its saved configuration and stored data. Update settings start fresh (auto-update on, no pinned version), and widgets, overlay layers and sounds that were deleted at uninstall are not restored. Built-in system extensions cannot be uninstalled, and Lumio's automatic system-extension setup never restores an install that was uninstalled.
System extensions
System extensions replace the former built-in plugin system. They provide the same functionality (chat moderation modules, overlay widgets) but run through the standard extension infrastructure (V8 isolates, Extension Supervisor iframes).
Built-in widgets
| Extension | Slug | Default Install | Description |
|---|---|---|---|
| Chat Box | system-chat-box | Yes | Live chat feed from connected platforms |
| Alert Box | system-alert-box | Yes | Animated alerts for follows, subs, raids |
| Event List | system-event-list | Yes | Scrolling list of recent events |
| Text | system-text | Yes | Labels, titles, and text overlays |
| Image | system-image | Yes | Upload or URL-based image overlays |
| Timer | system-timer | Yes | Countdown / stopwatch timer |
| Spotify Now Playing | system-spotify-now-playing | No | Displays the currently playing Spotify track |
| Browser Source | system-obs-browser-source | No | Headless OBS integration events |
Built-in bot modules
| Extension | Slug | Default Install | Description |
|---|---|---|---|
| Link Protection | system-link-protection | Yes | Block or filter URLs in chat |
| Spam Protection | system-spam-protection | Yes | Detect excessive caps, symbols, emote spam |
| Word Filter | system-word-filter | Yes | Block banned words, phrases, or regex patterns |
| Timed Messages | system-timed-messages | Yes | Send messages on a rotating schedule |
Properties
visibility = 'system'- No developer or team owner (
developer_idandteam_idare NULL) - System-extension lookups require both owner fields to be NULL as well as the reserved slug, so developer-owned extensions are not selected as built-ins.
- Always free (
pricing_type = 'free') - Cannot be uninstalled (can be disabled via config)
- Cannot be deleted by users or developers
- 10 of 12 are auto-installed for new accounts on creation
lumio.config.jsonis the single source of truth - the API reads each manifest at compile time viainclude_str!(no hardcoded Rust structs)- All manifests include an
iconfield (Lucide icon name) - In local dev, bundles are uploaded from the filesystem; in production, deployed via CI (
lumio deploy --system)
Config schema
The config_schema field in lumio.config.json defines user-facing settings. The SchemaEditor component auto-generates a settings panel from this schema. CONFIG_FIELD_TYPES in packages/extension-types/src/config.ts defines the 18 supported field types:
| Type | Description |
|---|---|
string | Single-line text input |
number | Numeric input |
boolean | Toggle switch |
color | Color picker with hex input |
select | Dropdown with predefined options |
textarea | Multi-line text input |
slider | Range slider with min/max/step |
font | Font family picker (Google Fonts catalog) |
icon | Icon picker (Lucide, emoji, emotes, uploads) |
image | Image upload or URL |
sound | Sound file upload or URL |
multiselect | Multiple selection from options |
group | Collapsible group of nested fields |
divider | Visual separator |
info | Read-only informational text |
channel | Multi-platform channel picker |
designer | Fullpage designer surface field |
presets | Selectable preset bundles that write several other fields at once |
Field properties
| Property | Applies to | Description |
|---|---|---|
label | All | Display label |
default | All value types | Default value |
required | All value types | Validation requirement |
min / max / step | slider, number | Numeric range constraints |
placeholder | string, textarea | Input placeholder text |
rows | textarea | Number of visible rows |
unit | slider | Unit suffix (e.g. "px", "ms") |
collapsed | group | Start collapsed |
visible_when | All | Conditional visibility based on other field values |
fields | group | Nested child field definitions |
content | info | Informational text content |
options | select, multiselect | Available options array |
multiple | multiselect | Allow multiple selections |
presets | designer, presets | Predefined preset configurations |
A separate designer_schema array in lumio.config.json defines fields for the fullpage designer surface.
Server-side validation
Config values are validated against the config_schema before writing to extension_installs.config. Validates type match, required fields, select options, color hex format, slider range bounds, and rejects unknown keys. Both GraphQL and REST enforce validation.
Widget editor
An extension widget instance is bound to a live installation of that extension in the active account. The instance takes its extension identity from the installation; an installation from another account or one already uninstalled cannot be used. Each widget has its own access grants: no grant, viewer, and editor allow it in the widget list; none hides it. Copying requires Editor access to the source and widgets:create. widgets:access-read lets an account member see every widget but does not grant copy access; widgets:access-grant bypasses the per-widget copy check.
Two rendering paths produce the same @lumio/ui components:
- config_schema -- The host-side
SchemaEditorcomponent auto-generates form fields directly from the schema definition - editor.tsx -- SDK Editor Components (EditorInput, EditorTextarea, EditorSelect, EditorCheckbox, EditorSlider, EditorColorPicker, EditorGroup, EditorLabel, EditorDivider, EditorInfo, EditorFontRow, EditorButton) serialize via the Worker Reconciler and render as
@lumio/uion the host
Extensions can use config_schema only, editor.tsx only, or both (schema fields render first, custom editor below).
Advanced settings
The AdvancedSettings panel provides:
- Layout: Width, height, rotation, optional X/Y positioning
- Appearance: Opacity
- Corner Radius: Linked (all corners) or individual per-corner control
- Effects: Text shadow, layer blur, background blur
- Strokes: Border styling
- Custom CSS: Fullpage Monaco Editor modal
Extension designer
The ExtensionDesigner is a fullpage overlay with a split pane (preview + settings). It supports both designer_schema (host-side form from lumio.config.json) and designer.tsx (Worker pipeline via SDK Editor Components).
Font proxy
DSGVO-compliant font serving through Lumio infrastructure. No browser requests go to Google.
| Endpoint | Description |
|---|---|
GET /v1/fonts/\{family\}/css | CSS stylesheet with @font-face rules pointing to self-hosted woff2 URLs |
GET /v1/fonts/\{family\}/\{weight\}.woff2 | Proxied font file, cached via lo-storage |
Both endpoints are public scope (no auth required) with immutable cache headers.
Store integration
System extensions appear in the Store alongside community extensions:
- Category filter: Top-3 category chips (All, Built-in, Widgets) plus a "More" dropdown for remaining categories. Sort by Popular, Recent, or Top Rated.
- Built-in filter: A dedicated "Built-in" category shows all system extensions across categories, sorted before community extensions.
- Installed badge: Each card shows a green corner check-mark when the extension is already installed on the current account.
- Detail page: Shows "Built-in by Lumio" instead of a developer panel, with category badge, Lucide icon, and install/uninstall actions.
- Pagination: Load More button fetches the next 20 results.
- Install: Normal install flow. System extensions cannot be uninstalled.
- Icons: An
icon_keythat names a Lucide icon (all system extensions) renders that icon without a request. An uploaded icon loads through the dashboard proxy/api/extensions/{slug}/icon, which serves only store-visible extensions (icon-databelow). Drafts and private extensions therefore show their letter placeholder in the store, the installed list, the developer's extension list and overview, the bot-module list and the widget picker, never a broken image.
Bundle files
Bundle files support JavaScript, CSS, HTML, source maps, raster images, SVG, fonts, media, JSON, and Lottie. Every served file has X-Content-Type-Options: nosniff. HTML, SVG, JSON, source maps, Lottie, and other non-passive content also have Content-Security-Policy: sandbox and Content-Disposition: attachment; the dashboard bundle proxy applies the same headers. SVG can still be loaded as an image resource.
Discovery API
| Endpoint | Description |
|---|---|
GET /v1/extensions?visibility=system&category=widget | List system widget extensions |
GET /v1/extensions?visibility=system&category=bot_module | List system bot module extensions |
GET /v1/extensions/{slug} | Store detail for one extension |
GET /v1/extensions/{slug}/reviews · POST /v1/extensions/{slug}/reviews | Read / write store reviews |
GET /v1/extension-installs · POST /v1/extension-installs · DELETE /v1/extension-installs/{id} | Per-account install lifecycle |
GET /v1/extension-installs/{install_id}/storage · POST …/storage | Per-install extension storage (useExtensionStorage()) |
GET /v1/extension-installs/{id}/logs | Per-install handler logs of a bot-module or automation-node install: bot-modules:read (bot module) or automations:read (automation node), resolved from the install's category; every rejection is 404 Install not found. Messages are scrubbed; automation inputData is returned as shape only |
GET /v1/store/extensions/{slug}/icon · .../screenshots | Store media assets |
GET /v1/store/extensions/{id}/icon-data | Icon image bytes (PNG/JPEG/WebP) of a store-visible extension; every extension icon_url points here |
GET /v1/platforms/metadata | Static platform info |
platformMetadata (GraphQL) | Same as REST, public query |
GraphQL counterpart
Every store operation is also a GraphQL field. All store fields combine the
feature:extensions flag with an extension-store:* permission;
extensionInstallLogs is gated by the install's category permission instead:
| Field | Kind | Permission |
|---|---|---|
storeExtensions · storeExtension | Query | feature:extensions + extension-store:read |
installedExtensions | Query | feature:extensions + extension-store:read |
extensionVersions · extensionReviews · extensionBundleUrl | Query | feature:extensions + extension-store:read |
uninstallImpact | Query | feature:extensions + extension-store:uninstall |
installExtension | Mutation | feature:extensions + extension-store:install |
uninstallExtension | Mutation | feature:extensions + extension-store:uninstall |
configureExtension · setExtensionAutoUpdate · pinExtensionVersion | Mutation | feature:extensions + extension-store:configure |
extensionInstallLogs | Query | bot-modules:read (bot-module install) or automations:read (automation-node install); same guard, redaction and Install not found error as REST |
reviewExtension | Mutation | feature:extensions + extension-store:review |
extensionStorage | Query | feature:extensions + extension-store:read (or the install's extension token, or an overlay/widget token that renders the install) |
setExtensionStorage | Mutation | feature:extensions + extension-store:configure (or the install's extension token); overlay and widget tokens never write |
WebSocket
| Channel | Gate | Feature flag |
|---|---|---|
ext-storage:{install_id} | The install's extension token, extension-store:read in the account that owns the install, or an overlay/widget token that renders the install - the read rule of extensionStorage and its REST twin | feature:extensions |
ext-logs:{extension_id} | Extension owner (developer or team member) | feature:extension_development |
ext-install-logs:{install_id} | bot-modules:read (bot-module install) or automations:read (automation-node install), resolved from the install's category like the GraphQL/REST twins | feature:bot_module_extensions |