DeveloperPlugin Iframe Protocol

Plugin Iframe Protocol Reference

Bidirectional communication between StudioBrain host and plugin iframes

Overview

Plugins can register UI panels that render inside sandboxed iframes within the entity editor. Communication between the host app and plugin iframes uses the browser postMessage API with structured typed messages.

All types are defined in src/lib/plugin-message-protocol.ts.

Host to Plugin Messages

Sent by StudioBrain to the plugin iframe.

entity-context

Sent when the iframe loads. Provides the plugin with entity data and environment info.

{
  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"
}

theme-change

Sent when the user toggles between light and dark mode.

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

entity-updated

Sent when entity data changes (e.g., after a save or field update).

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

Plugin to Host Messages

Sent by the plugin iframe to StudioBrain.

entity-modified

Request to update entity fields. The host merges these into the current entity data.

{
  type: 'entity-modified';
  fields: Record<string, any>;  // e.g., { description: "Updated text" }
}

Request to navigate the host application to a different route.

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

toast

Request to display a toast notification in the host UI.

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

resize

Request to resize the iframe container height.

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

Security

Origin Validation

  • The entity-context message includes hostOrigin so plugins know which origin to trust
  • The host validates event.origin before processing any incoming plugin messages
  • Iframes use the sandbox attribute: allow-scripts allow-same-origin allow-forms

Type Guards

import { isPluginMessage, isHostMessage } from '@/lib/plugin-message-protocol';
 
// In host code — validate incoming plugin messages
window.addEventListener('message', (event) => {
  if (isPluginMessage(event.data)) {
    // Safe to handle event.data as PluginToHostMessage
  }
});
 
// In plugin code — validate incoming host messages
window.addEventListener('message', (event) => {
  if (isHostMessage(event.data)) {
    // Safe to handle event.data as HostToPluginMessage
  }
});

Plugin Usage Example

Receiving Entity Context

window.addEventListener('message', (event) => {
  if (event.data?.type === 'entity-context') {
    const { entityType, entityId, entityData, theme } = event.data;
    // Initialize plugin UI with entity data
    renderPluginUI(entityData, theme);
  }
 
  if (event.data?.type === 'theme-change') {
    applyTheme(event.data.theme);
  }
 
  if (event.data?.type === 'entity-updated') {
    // Refresh plugin UI with new data
    renderPluginUI(event.data.entityData);
  }
});

Updating Entity Data

// Request field update
window.parent.postMessage({
  type: 'entity-modified',
  fields: { description: 'Updated by plugin' }
}, '*');

Auto-Resize

// Notify host of content height changes
const observer = new ResizeObserver(() => {
  window.parent.postMessage({
    type: 'resize',
    height: document.body.scrollHeight
  }, '*');
});
observer.observe(document.body);
// Navigate to another entity
window.parent.postMessage({
  type: 'navigate',
  path: '/characters/rex_marshall'
}, '*');
 
// Show success toast
window.parent.postMessage({
  type: 'toast',
  message: 'Analysis complete!',
  toastType: 'success'
}, '*');

Plugin Registration

Plugins declare panels in their plugin.json manifest:

{
  "name": "my-plugin",
  "capabilities": {
    "frontend": {
      "panels": [
        {
          "id": "analysis-panel",
          "title": "Entity Analysis",
          "location": "entity-sidebar",
          "entity_types": ["character", "location"],
          "url": "/panels/analysis.html"
        },
        {
          "id": "stats-tab",
          "title": "Statistics",
          "location": "entity-tab",
          "url": "/panels/stats.html"
        }
      ]
    }
  }
}

Panel Locations

LocationRenders AsDescription
entity-sidebarCollapsible sectionShown alongside Visual Editor tab
entity-tabFull tabAuto-injected into EntityEditTabs
entity-footerBelow editorReserved for future use

Component Block Registration

Plugin panels can also be placed as component blocks in layouts. The component ID format is plugin:pluginId:panelId, e.g., plugin:my-plugin:analysis-panel.

These are rendered by ComponentBlockRenderer which delegates to PluginBlockIframe for any component ID matching the plugin:*:* pattern.

Field Widget Protocol

Field widgets use a simplified protocol compared to panels:

sb-field-widget-update (Host → Widget)

Sent when the field value changes or on initial load.

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

sb-field-widget-change (Widget → Host)

Sent when the user changes the value.

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

sb-field-widget-resize (Widget → Host)

Sent to adjust iframe height (constrained to 40-300px).

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