Appearance
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
localStorageis 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:
| Field | What it is |
|---|---|
componentId / componentName | The node type's ID and display name. |
nodeId | The specific node instance on the canvas. |
initialData | The node's currently saved config. |
theme | { palette, mode } — so the form matches the editor's light/dark theme. |
project | Optional: projectId, fileId, fileName, folderPath. |
availableInputs | Optional: 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:
| Method | Permission | What it does |
|---|---|---|
getContext | (none) | Return the SandboxContext. Always allowed. |
getProjectData | project.read | Read the node's current config. |
setProjectData | project.write | Save updated config for the node. |
dispatchEvent | project.write | Emit a named event to the host. |
requestApiCall | network.declared | Ask 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:
| Code | Meaning |
|---|---|
PERMISSION_DENIED | The plugin didn't declare/approve the required permission. |
ENDPOINT_NOT_DECLARED | requestApiCall target isn't in manifest.network. |
UNKNOWN_METHOD | The method name isn't recognised. |
HANDLER_ERROR | The 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()— sendsguest:readywith auto-retry untilinitarrives.onInit(handler)— registers the single-fire init handler and applies theme tokens.rpc(method, params)— sends a request, returns a promise.persister({ getData })— a debouncedsetProjectDatahelper.applyTheme(theme)/debounce(fn, ms)— utilities.
See node authoring for how these fit together in a real config UI.
See also
- Authoring a node addon — building the config UI that speaks this protocol
- Reference overview — the two halves of a node, host vs. plugin
- Editing on the canvas — where the config UI appears in the flow editor