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?
| Kind | Desktop | Mobile | Cloud |
|---|---|---|---|
| Official universal | Component (Cranelift) + these panels | Same Component (Pulley) + these panels | Official Worker at our deploy + these panels |
| Official desktop-only | Component + network:local + panels | — | — |
| Community plugin | Component + panels | Same Component + panels | These panels and/or the author’s backend. No Worker on BiloxiStudios Cloudflare. |
| Any data kind | Files in the project | Files in the project | Files 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.
navigate
{
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-contextincludeshostOriginso the panel knows which parent to trust.- The host validates
event.originbefore 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"
}
]
}
}
}| Location | Renders as |
|---|---|
entity-sidebar | Collapsible section |
entity-tab | Entity edit tab |
entity-footer | Below 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
}