DeveloperPlugin Iframe Protocol

Plugin Iframe Protocol Reference

Bidirectional postMessage between the StudioBrain host and plugin iframes. This is the panel contract — the UI artifact that runs on desktop, mobile, and cloud.

Guest execution is a different path. See Architecture.

Where does it run?

KindDesktopMobileCloud
Official universalComponent (Cranelift) + these panelsSame Component (Pulley) + these panelsOfficial Worker at our deploy + these panels
Official desktop-onlyComponent + network:local + panels
Community pluginComponent + panelsSame Component + panelsThese panels and/or the author’s backend. No Worker on BiloxiStudios Cloudflare.
Any data kindFiles in the projectFiles in the projectFiles in the project. No Worker.

Community cloud inside StudioBrain is this protocol. The host never WebAssembly.compiles fetched plugin bytes to “make the panel’s guest run.”

Types live in src/lib/plugin-message-protocol.ts.

Host to plugin

entity-context

Sent when the iframe loads.

{
  type: 'entity-context';
  entityType: string;       // e.g. "character"
  entityId: string;         // e.g. "rex_marshall"
  entityData: Record<string, any>;
  theme: 'light' | 'dark';
  hostOrigin: string;       // e.g. "http://localhost:3000"
}

Some host builds also send data as an alias of entityData. Read both if you need to be defensive.

theme-change

{
  type: 'theme-change';
  theme: 'light' | 'dark';
}

entity-updated

{
  type: 'entity-updated';
  entityType: string;
  entityId: string;
  entityData: Record<string, any>;
}

Plugin to host

entity-modified

{
  type: 'entity-modified';
  fields: Record<string, any>;
}

The host merges fields into the current entity. Some hosts also accept changes as an alias.

{
  type: 'navigate';
  path: string;  // e.g. "/characters/rex_marshall"
}

toast

{
  type: 'toast';
  message: string;
  toastType: 'success' | 'error' | 'info';
}

resize

{
  type: 'resize';
  height: number;  // pixels
}

request-entity-data

Ask the host to send entity-context again.

{
  type: 'request-entity-data';
}

Security

  • entity-context includes hostOrigin so the panel knows which parent to trust.
  • The host validates event.origin before handling plugin messages.
  • Iframes use sandbox="allow-scripts allow-same-origin allow-forms".
  • This protocol does not grant a cloud Worker. Community guests still do not run on BiloxiStudios Cloudflare.

Type guards

import { isPluginMessage, isHostMessage } from '@/lib/plugin-message-protocol';
 
window.addEventListener('message', (event) => {
  if (isPluginMessage(event.data)) {
    // PluginToHostMessage
  }
});
 
window.addEventListener('message', (event) => {
  if (isHostMessage(event.data)) {
    // HostToPluginMessage
  }
});

Panel usage

Receive context

window.addEventListener('message', (event) => {
  if (event.data?.type === 'entity-context') {
    const entity = event.data.entityData ?? event.data.data;
    renderPluginUI(entity, event.data.theme);
  }
  if (event.data?.type === 'theme-change') {
    applyTheme(event.data.theme);
  }
  if (event.data?.type === 'entity-updated') {
    renderPluginUI(event.data.entityData ?? event.data.data);
  }
});

Update fields, resize, navigate

window.parent.postMessage({
  type: 'entity-modified',
  fields: { description: 'Updated by plugin' }
}, '*');
 
new ResizeObserver(() => {
  window.parent.postMessage({
    type: 'resize',
    height: document.body.scrollHeight
  }, '*');
}).observe(document.body);
 
window.parent.postMessage({
  type: 'navigate',
  path: '/characters/rex_marshall'
}, '*');
 
window.parent.postMessage({
  type: 'toast',
  message: 'Analysis complete!',
  toastType: 'success'
}, '*');

Use semantic --surface-* variables. See Plugin theme compliance.

Registration

Declare panels in plugin.json:

{
  "id": "my-plugin",
  "capabilities": {
    "frontend": {
      "panels": [
        {
          "id": "analysis-panel",
          "title": "Entity Analysis",
          "location": "entity-sidebar",
          "entity_types": ["character", "location"],
          "url": "/panels/analysis.html"
        }
      ]
    }
  }
}
LocationRenders as
entity-sidebarCollapsible section
entity-tabEntity edit tab
entity-footerBelow editor (reserved)

Layout block id: plugin:{pluginId}:{panelId}PluginBlockIframe.

Field widget protocol

Smaller iframe, same origin rules.

sb-field-widget-update (host → widget)

{
  "type": "sb-field-widget-update",
  "value": "current value",
  "disabled": false,
  "options": []
}

sb-field-widget-change (widget → host)

{
  "type": "sb-field-widget-change",
  "value": "new value"
}

sb-field-widget-resize (widget → host)

Height clamped to 40–300px.

{
  "type": "sb-field-widget-resize",
  "height": 120
}

See also