Developer ReferencePlugin Iframe Protocol

Plugin Iframe Protocol Reference

Bidirectional communication between the StudioBrain host application 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.

Theme compliance note: Plugin iframes do not inherit the host’s CSS variables. Plugins must implement their own theme handling using the surface variable pattern. See the Plugin Development page for mandatory theme compliance requirements.

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"
  data: Record<string, any>; // Full entity fields + markdown_body
}

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';
  data: Record<string, any>;
}

Plugin to Host Messages

Sent by the plugin iframe to StudioBrain.

request-entity-data

Request the current entity data. The host responds with an entity-context message.

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

entity-modified

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

{
  type: 'entity-modified';
  changes: 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';
  level: 'success' | 'error' | 'info';
  message: string;
}

resize

Request to resize the iframe container height.

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

Security

Origin Validation

  • The host validates event.origin before processing any incoming plugin messages.
  • Plugins should validate that messages come from the expected host origin.
  • 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, data } = event.data;
    // Initialize plugin UI with entity data
    renderPluginUI(data);
  }
 
  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.data);
  }
});

Updating Entity Data

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

Requesting Fresh Data

// Ask the host for the latest entity data
window.parent.postMessage({
  type: 'request-entity-data'
}, '*');

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',
  level: 'success',
  message: 'Analysis complete!'
}, '*');

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.

TypeScript Type Definitions

The complete type definitions from src/lib/plugin-message-protocol.ts:

// Host to Plugin
type HostToPluginMessage =
  | EntityContextMessage    // { type: 'entity-context', entityType, entityId, data }
  | ThemeChangeMessage      // { type: 'theme-change', theme }
  | EntityUpdatedMessage;   // { type: 'entity-updated', data }
 
// Plugin to Host
type PluginToHostMessage =
  | RequestEntityDataMessage  // { type: 'request-entity-data' }
  | EntityModifiedMessage     // { type: 'entity-modified', changes }
  | NavigateMessage           // { type: 'navigate', path }
  | ToastMessage              // { type: 'toast', level, message }
  | ResizeMessage;            // { type: 'resize', height }