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.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 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 (ext.lumio.vision)

The Extension Runtime is another minimal vanilla JavaScript page hosted at a separate subdomain. It:

  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).

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 — 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
Respond to theme changesuseLumioTheme() 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 requestsOnly ctx.fetch() with declared egress hosts (server functions)
Access viewer cookies or JWT tokensDifferent origin; cookies are not sent to ext.lumio.vision
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

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_URL and NEXT_PUBLIC_EXTENSION_RUNTIME_URL env vars point to reachable origins.
  • 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.