Sandbox Architecture
Lumio runs every extension inside a multi-layer sandbox. Understanding the sandbox helps you reason about what extensions can and cannot do, and makes debugging straightforward when something unexpected happens.
Why sandboxing matters
Extensions are third-party code executed inside the viewer's browser and Lumio's servers. Without isolation, a malicious or buggy extension could:
- Read or write another extension's storage
- Send arbitrary network requests using the viewer's session
- Crash or freeze the host overlay page
- Access sensitive viewer data (tokens, cookies, clipboard)
The Lumio sandbox prevents all of the above at the architecture level rather than relying on code review alone.
3-Layer architecture
Host (lumio.vision)
└─ Supervisor iframe (supervisor.ext.lumio.vision)
└─ Extension Runtime iframe (<label>.runtime.ext.lumio.vision, one origin per extension)
└─ Web Worker (lumio-runtime.js + your extension bundle)
Each layer runs on a distinct origin enforced by the browser's same-origin policy. postMessage is the only communication channel between layers; there is no shared memory, no shared DOM, and no shared storage.
Layer 1 - Host (lumio.vision)
The host page is the Lumio overlay, widget, or dashboard editor. It owns the real DOM and renders what viewers see. The host communicates exclusively with the Supervisor iframe and never touches extension code directly.
The host creates the Supervisor iframe via ExtensionWorkerManager and sends messages to supervisorIframe.contentWindow.postMessage(...).
Layer 2 - Supervisor (supervisor.ext.lumio.vision)
The Supervisor is a minimal (~90-line) vanilla JavaScript page hosted at a separate subdomain. It acts as a message firewall:
- Messages arriving from the Extension Runtime (Worker → Host direction) are validated against an allowlist of permitted JSON-RPC methods before being forwarded to the Host.
- Messages arriving from the Host (Host → Worker direction) are validated against a second allowlist before being forwarded to the Extension Runtime.
- Any message with an unknown or disallowed
methodis dropped with a console warning. It is never forwarded in either direction.
JSON-RPC response objects (those with an id field but no method) are forwarded without method validation, as they are replies to explicit requests.
Permitted Worker → Host methods:
ui.render, ui.ready, ui.keyframes, ui.error, storage.set, storage.get, server.query, server.mutation, action.execute
Permitted Host → Worker methods:
lifecycle.init, lifecycle.destroy, event.callback, event.platform, storage.update, theme.change
Layer 3 - Extension Runtime (<label>.runtime.ext.lumio.vision)
The Extension Runtime is another minimal vanilla JavaScript page. Every extension gets its own Runtime origin: <label> is your extension's UUID as 32 lowercase hex characters, for example https://0f9c2d1e8b7a46c3a5e4d3c2b1a09f8e.runtime.ext.lumio.vision. The Supervisor only loads an extension from the origin bound to its id, and the Runtime page refuses to start a Worker on any other host. The page:
- Blocks
localStorageandsessionStorageentirely viaObject.defineProperty- throwing aDOMExceptionon access. - Creates the Web Worker (
new Worker(runtimeUrl)) where your actual extension code runs. - Forwards all messages from the Worker to the Supervisor (parent) and vice versa, without additional validation (validation is done at the Supervisor layer).
Because every extension has its own origin, the browser itself keeps the client-side state of two extensions apart (storage, caches, locks, channels, service workers are all per origin). The storage blocking stays as defence in depth and as the SDK contract: instances of the same extension (for example two accounts in one browser profile) still share an origin, and extensions keep no client-side state at all. The useExtensionStorage SDK hook uses Lumio's Redis-backed storage instead, which is properly scoped per extension.
Web Worker
Your extension bundle and the Lumio runtime (lumio-runtime.js) run inside a Web Worker. This means:
- No DOM access at all -
documentandwindowdo not exist in this context. - React renders through a custom reconciler that serialises the component tree to
LumioComponentNode[]and sends it to the Host viaui.render- the Host renders the actual DOM elements. - Network requests, storage reads/writes, and server function calls all go through the SDK which routes them via
postMessagethrough the Supervisor.
What extensions can do
| Capability | How |
|---|---|
| Render UI components | Box, Text, Button, Image, etc. via @zaflun/lumio-sdk |
| Read and write extension storage | useExtensionStorage() hook |
| Call server functions | useQuery(), useMutation() hooks |
| Receive platform events | useLumioEvent() hook |
| Trigger custom actions | useLumioAction() hook |
| Read the overlay/widget theme | useLumioTheme() hook |
| Animate UI elements | defineKeyframes(), useAnimation() |
What extensions cannot do
| Action | Reason |
|---|---|
| Access the DOM directly | Web Worker has no DOM context |
Use localStorage / sessionStorage | Blocked by the Extension Runtime |
| Make arbitrary network requests | Bot-module ctx.fetch() reaches only declared egress hosts; server functions and automation nodes cannot make outbound requests |
| Access viewer cookies or JWT tokens | Different origin from the Lumio app; document.cookie is neutered in the Runtime and a Worker has no cookie access. See Cookies |
| Navigate the top-level page | No access to window.top or window.parent.location |
| Send undeclared JSON-RPC methods | Supervisor drops unknown methods |
| Access another extension's data | Storage is scoped per extension; no shared globals |
Cookies
The Runtime origins are same-site with lumio.vision (they share the registrable domain), but not same-origin. Keeping the viewer's session cookies away from the Runtime origins requires host-only cookies: with LUMIO_COOKIE_DOMAIN / COOKIE_DOMAIN unset, Lumio sets them without a Domain attribute, so the browser sends them only to the Lumio host that set them. A deployment that sets a parent cookie domain covering the Runtime hosts (for example .lumio.vision) sends the session cookies along with every request to a Runtime host. Even then extension code cannot read them - document.cookie returns "" in the Runtime page, a Worker has no cookie API, and the Worker's CSP blocks every network request - and the Lumio API never accepts an extension sandbox origin for CORS.
How this affects development
The sandbox is transparent to extension developers. The lumio dev CLI sets up local equivalents of all three layers automatically:
- A local Supervisor page at
http://localhost:3200 - A local Extension Runtime page at
http://localhost:3201 - Your extension bundle via the Vite dev server at
http://localhost:5173
You write the same @zaflun/lumio-sdk components and hooks as always. The SDK handles all cross-layer communication internally.
Debugging tips
Viewing the iframe tree
Open Chrome DevTools → Elements panel. You will see:
body
└─ iframe (src: supervisor.ext.lumio.vision or localhost:3200)
└─ iframe (src: <label>.runtime.ext.lumio.vision or <label>.localhost:3201)
Each iframe has its own DevTools context. Use the context switcher (top-left of the Console panel) to select the Supervisor or Runtime iframe.
Supervisor console messages
The Supervisor logs a warning when it drops a message:
[Supervisor] Blocked worker→host message: some.unknown.method
[Supervisor] Blocked host→worker message: some.unknown.method
If you see these messages, a method your extension is trying to use is not in the allowlist. This means either:
- You are calling an internal method that is not part of the SDK's public protocol.
- A version mismatch exists between your extension bundle and the installed runtime.
Worker errors
Uncaught errors in the Web Worker surface as ui.error messages on the Host. You can also inspect them by selecting the Worker context in DevTools → Sources → Threads.
iframe not loading
If the Supervisor or Extension Runtime iframe fails to load:
- Verify the web app's
LUMIO_SUPERVISOR_URL,LUMIO_EXTENSION_RUNTIME_URLandLUMIO_EXTENSION_RUNTIME_ORIGIN_TEMPLATEenv vars (read per request;NEXT_PUBLIC_*names are the build-time fallback) point to reachable origins. With a template, each extension loads from its own origin (<32 hex>.<runtime host>), so the runtime host needs a wildcard DNS record and certificate. - Check that
CORSheaders allow the host origin. The static apps are served by Cloudflare Pages which sends permissive CORS headers by default. - In local dev, ensure
just dev-supervisor(port 3200) andjust dev-runtime(port 3201) are running.