Skip to main content

Extension Supervisor

The Extension Supervisor is a 3-layer isolation system that sandboxes extension code using cross-origin iframes and a Web Worker. Two minimal static apps — the Supervisor and the Extension Runtime — enforce origin boundaries and message validation between the Lumio host page and untrusted extension bundles.

Architecture

Host (lumio.vision)
└─ Supervisor iframe (supervisor.extension.lumio.vision)
└─ Extension Runtime iframe (ext.lumio.vision)
└─ Web Worker (lumio-runtime.js + extension bundle)

Each layer runs on a distinct origin. The browser's same-origin policy ensures no layer can access another layer's DOM, cookies, or JavaScript globals. All communication travels via postMessage.

Why three layers

LayerOriginPurpose
Hostlumio.visionRenders the real DOM; owns overlay/widget/editor pages
Supervisorsupervisor.extension.lumio.visionValidates messages against an allowlist; proxies approved messages
Extension Runtimeext.lumio.visionHosts the Web Worker; blocks browser storage
Web Worker(spawned by Runtime)Runs extension code with no DOM access

A two-layer design (Host → Runtime → Worker) would let extension code send arbitrary JSON-RPC methods to the Host if the Runtime were compromised. The Supervisor layer is the enforcement point that makes the allowlist tamper-resistant — it runs on a different origin the extension code cannot reach.

Domain setup

EnvironmentSupervisorExtension Runtime
Productionsupervisor.extension.lumio.visionext.lumio.vision
Staginglumio-extension-supervisor-staging.pages.dev (CF Pages)lumio-extension-runtime-staging.pages.dev
Local devhttp://localhost:3200http://localhost:3201

Both production domains require DNS records pointing to their respective Cloudflare Pages projects and matching custom domain configuration in the CF Pages dashboard.

CF Pages deployment

Both apps are vanilla HTML/CSS/JS with no build step. The deploy workflow at .github/workflows/deploy-extension-apps.yml deploys them to separate CF Pages projects on pushes to next (staging) and main (prod-preview), and on version tags 20* (production).

Branch/TagSupervisor projectRuntime project
nextlumio-extension-supervisor-staginglumio-extension-runtime-staging
mainlumio-extension-supervisor-prodlumio-extension-runtime-prod
20* tagslumio-extension-supervisorlumio-extension-runtime

The workflow uses secrets.CF_API_TOKEN and secrets.CF_ACCOUNT_ID (same secrets as all other CF Pages workflows). No build step or pnpm install is needed — the apps are pure static files.

Message validation allowlist

The Supervisor in apps/extension-supervisor/script.js defines two sets:

Worker → Host (extension sends, host receives)

const ALLOWED_WORKER_TO_HOST = new Set([
"ui.render",
"ui.ready",
"ui.keyframes",
"ui.error",
"storage.set",
"storage.get",
"server.query",
"server.mutation",
"action.execute",
]);

Host → Worker (host sends, extension receives)

const ALLOWED_HOST_TO_WORKER = new Set([
"lifecycle.init",
"lifecycle.destroy",
"event.callback",
"event.platform",
"storage.update",
"theme.change",
]);

Messages with method values outside these sets are dropped. JSON-RPC response objects (with id but no method) bypass method validation — they are replies to explicit requests and must always pass through.

When adding a new JSON-RPC method to the protocol, you must also add it to the appropriate allowlist in apps/extension-supervisor/script.js. The Supervisor is deployed independently; a deploy of the Supervisor alone is sufficient (no web app deploy needed).

Security properties

PropertyMechanism
Origin isolationThree distinct origins; browser same-origin policy blocks cross-frame access
Message allowlistSupervisor drops any method not in the allowlist
Storage isolationExtension Runtime blocks localStorage/sessionStorage via Object.defineProperty
Worker isolationWeb Worker has no DOM, no document, no window.location navigation
Network isolationExtensions use ctx.fetch() in server functions (egress allowlist enforced in Rust)
CSPSupervisor injects a frame-src CSP meta tag at runtime, allowing only the Extension Runtime origin

The Extension Runtime blocks browser storage because all extensions share the ext.lumio.vision origin. Without the block, Extension A could read Extension B's localStorage. Extensions use useExtensionStorage (backed by Redis, scoped per extension) instead.

Dev mode setup

Install serve

npm install -g serve
# or use npx (no install needed)

Run both apps

# Terminal 1
just dev-supervisor # http://localhost:3200

# Terminal 2
just dev-runtime # http://localhost:3201

Configure the web app

Set these env vars in apps/web/.env.local:

NEXT_PUBLIC_SUPERVISOR_URL=http://localhost:3200
NEXT_PUBLIC_EXTENSION_RUNTIME_URL=http://localhost:3201

These fall back to the production URLs when not set, so production deployments require no local overrides.

Start the full dev stack

just stack-up # PostgreSQL, TimescaleDB, Redis
just run-api # Rust API
just dev-web # Next.js web app
just dev-supervisor # Extension Supervisor
just dev-runtime # Extension Runtime

Troubleshooting

Message not forwarded (blocked by Supervisor)

The Supervisor logs a warning:

[Supervisor] Blocked worker→host message: some.unknown.method
[Supervisor] Blocked host→worker message: some.unknown.method

Solution: add the method to the appropriate set in apps/extension-supervisor/script.js and redeploy the Supervisor.

Extension iframe does not load

  1. Verify NEXT_PUBLIC_SUPERVISOR_URL and NEXT_PUBLIC_EXTENSION_RUNTIME_URL are set correctly in the environment.
  2. Check that the CF Pages project exists and has the correct custom domain configured.
  3. In local dev, confirm just dev-supervisor and just dev-runtime are running.
  4. Check browser DevTools → Console for CORS errors. The static apps must send Access-Control-Allow-Origin headers — CF Pages does this by default.

Worker fails to start

The Extension Runtime sends a ui.error JSON-RPC notification when the Worker cannot be created:

{ "jsonrpc": "2.0", "method": "ui.error", "params": { "code": "WORKER_CREATE_FAILED", "message": "..." } }

The Host's ExtensionWorkerManager surfaces this via the onError callback. Common causes:

  • The runtimeUrl (lumio-runtime.js) is unreachable or blocked by CSP.
  • The Worker script fails to parse (syntax error in extension bundle).

Inspect the Extension Runtime iframe's console for more details (use the context switcher in Chrome DevTools).

Supervisor not ready (lifecycle.init never sent)

ExtensionWorkerManager waits for { type: "supervisor", status: "loaded" } before sending { type: "init", ... }. If the Supervisor iframe fails to load at all, the loaded signal is never emitted and the extension stays in a pending state.

Check:

  • Network tab in DevTools shows the Supervisor URL loading successfully (HTTP 200).
  • No CSP or mixed-content errors block the iframe.