Skip to main content

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 method is 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:

  1. Blocks localStorage and sessionStorage entirely via Object.defineProperty - throwing a DOMException on access.
  2. Creates the Web Worker (new Worker(runtimeUrl)) where your actual extension code runs.
  3. 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 - document and window do not exist in this context.
  • React renders through a custom reconciler that serialises the component tree to LumioComponentNode[] and sends it to the Host via ui.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 postMessage through the Supervisor.

What extensions can do​

CapabilityHow
Render UI componentsBox, Text, Button, Image, etc. via @zaflun/lumio-sdk
Read and write extension storageuseExtensionStorage() hook
Call server functionsuseQuery(), useMutation() hooks
Receive platform eventsuseLumioEvent() hook
Trigger custom actionsuseLumioAction() hook
Read the overlay/widget themeuseLumioTheme() hook
Animate UI elementsdefineKeyframes(), useAnimation()

What extensions cannot do​

ActionReason
Access the DOM directlyWeb Worker has no DOM context
Use localStorage / sessionStorageBlocked by the Extension Runtime
Make arbitrary network requestsBot-module ctx.fetch() reaches only declared egress hosts; server functions and automation nodes cannot make outbound requests
Access viewer cookies or JWT tokensDifferent 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 pageNo access to window.top or window.parent.location
Send undeclared JSON-RPC methodsSupervisor drops unknown methods
Access another extension's dataStorage 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_URL and LUMIO_EXTENSION_RUNTIME_ORIGIN_TEMPLATE env 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 CORS headers 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) and just dev-runtime (port 3201) are running.