Skip to content

Sandbox protocol

A node's config UI — the settings form you see when you select a node on the canvas — runs as an isolated web page inside a sandboxed iframe. It talks to the editor over a small postMessage RPC. This page documents that protocol, its permissions, and the trust boundary it enforces.

The trust boundary

The editor (the host) loads each config UI (the guest) into an iframe with sandbox="allow-scripts allow-same-origin", and each plugin is served from its own origin (a per-node subdomain like set.components.<host>). The browser treats every plugin as a separate origin, so:

  • Plugins cannot read the host's cookies or storage, or call host APIs as you.
  • Sibling plugins can't reach each other via shared storage or channels.
  • A plugin's localStorage is scoped to its own subdomain.

The config UI only ever edits a node's saved settings. It never runs the flow — flow execution is first-party server code (see node authoring). So even a misbehaving config UI can, at worst, write bad config into the one node it's attached to, gated by the permissions below.

The message shapes

Every message is a JSON object with a kind. There are four:

ts
// Guest → Host
{ kind: "guest:ready" }
{ kind: "rpc:req", id, method, params }

// Host → Guest
{ kind: "rpc:res", id, ok, result?, error? }
{ kind: "host:event", name, payload }

The host only accepts messages whose source is the plugin's own iframe, and it pins its replies to the iframe's exact origin — so host messages (which may carry project data) can never be delivered to an unexpected origin if the frame is navigated away.

Handshake

The guest signals guest:ready as soon as it loads (retrying until the host answers). The host replies with an init event carrying the SandboxContext. If the node's data later changes on the host side, the host sends an update event so the guest can re-sync.

SandboxContext

The init payload gives the guest everything it needs to render:

FieldWhat it is
componentId / componentNameThe node type's ID and display name.
nodeIdThe specific node instance on the canvas.
initialDataThe node's currently saved config.
theme{ palette, mode } — so the form matches the editor's light/dark theme.
projectOptional: projectId, fileId, fileName, folderPath.
availableInputsOptional: the fields upstream nodes expose, for building reference pickers.

RPC methods & permissions

The guest calls a method with rpc(method, params) and gets a promise back. Each method is gated by a permission the plugin must declare in its manifest and the host must have approved:

MethodPermissionWhat it does
getContext(none)Return the SandboxContext. Always allowed.
getProjectDataproject.readRead the node's current config.
setProjectDataproject.writeSave updated config for the node.
dispatchEventproject.writeEmit a named event to the host.
requestApiCallnetwork.declaredAsk the host to make an HTTP call on the plugin's behalf.

The full permission vocabulary is project.read, project.write, network.declared, storage.read, and storage.write. (The storage.* permissions are reserved for planned host-backed storage; no RPC method maps to them yet.)

requestApiCall and the network allowlist

requestApiCall is doubly gated. The plugin needs the network.declared permission and the target endpoint's origin must appear in the manifest's network allowlist — otherwise the call is rejected before any request is made. The host performs the fetch and returns { status, body }; the plugin never gets raw network access itself.

NOTE

The origin allowlist is enforced in the RPC layer today. Pinning it further with a browser-level CSP connect-src is planned.

Error responses

When an RPC can't be served, the response carries ok: false and a coded error:

CodeMeaning
PERMISSION_DENIEDThe plugin didn't declare/approve the required permission.
ENDPOINT_NOT_DECLAREDrequestApiCall target isn't in manifest.network.
UNKNOWN_METHODThe method name isn't recognised.
HANDLER_ERRORThe host handler threw while serving the call.

The shared runtime

Plugin authors don't hand-roll postMessage. The plugin repo ships a shared runtime (_runtime/runtime.js) that wraps all of the above:

  • ready() — sends guest:ready with auto-retry until init arrives.
  • onInit(handler) — registers the single-fire init handler and applies theme tokens.
  • rpc(method, params) — sends a request, returns a promise.
  • persister({ getData }) — a debounced setProjectData helper.
  • applyTheme(theme) / debounce(fn, ms) — utilities.

See node authoring for how these fit together in a real config UI.

See also

FlowRunner — the no-code platform for small businesses.