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" }
}navigate
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-contextmessage includeshostOriginso plugins know which origin to trust - The host validates
event.originbefore processing any incoming plugin messages - Iframes use the
sandboxattribute: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);Navigation and Toast
// 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
| Location | Renders As | Description |
|---|---|---|
entity-sidebar | Collapsible section | Shown alongside Visual Editor tab |
entity-tab | Full tab | Auto-injected into EntityEditTabs |
entity-footer | Below editor | Reserved 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
}