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.extension.lumio.vision)
└─ Extension Runtime iframe (ext.lumio.vision)
└─ 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.extension.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 (ext.lumio.vision)
The Extension Runtime is another minimal vanilla JavaScript page hosted at a separate subdomain. It:
- 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).
The storage blocking prevents cross-extension data leakage — all extensions share the ext.lumio.vision origin, so browser storage would be shared without this guard. 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 |
| Respond to theme changes | 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 | Only ctx.fetch() with declared egress hosts (server functions) |
| Access viewer cookies or JWT tokens | Different origin; cookies are not sent to ext.lumio.vision |
| 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 |
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.extension.lumio.vision or localhost:3200)
└─ iframe (src: ext.lumio.vision or 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
NEXT_PUBLIC_SUPERVISOR_URLandNEXT_PUBLIC_EXTENSION_RUNTIME_URLenv vars point to reachable origins. - 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.