DeveloperPlugin Development

Plugin Development

StudioBrain plugins add iframe panels, field widgets, and sandboxed guests. Official code lives in studiobrain-plugins. Community listings live on studiobrain-community (today studiobrain-community-plugins). Official data lives in studiobrain-templates only.

Canonical lock: Plugin Architecture.

Where does it run?

KindDesktopMobileCloud
Official universalWIT Component (wasmtime Cranelift)Same Component (wasmtime Pulley, SBAI-7745)Official Worker, compiled at our deploy. Panels on all surfaces.
Official desktop-onlyComponent + network:local
Community pluginComponentSame ComponentPanel iframe and/or the author’s backend. Never a Worker on BiloxiStudios Cloudflare.
Any data kindFiles in the projectFiles in the projectFiles in the project. No Worker.
⚠️

Do not WebAssembly.compile fetched R2 bytes in a Worker. WASI-on-Workers is not the StudioBrain WIT world. Community guests are not deployed onto our Cloudflare account.

Plugin manifest

plugin.json at the plugin root. Official proofs (entity-notes, entity-snapshots, content-stats) are the templates to copy.

{
  "id": "my-analytics-plugin",
  "name": "Entity Analytics",
  "version": "1.0.0",
  "description": "Visualize entity relationship networks and statistics",
  "author": "Your Name",
  "license": "Apache-2.0",
  "icon": "BarChart3",
  "trust_tier": "community",
  "type": "full",
  "capabilities": {
    "backend": {
      "entry": "plugin.wasm"
    },
    "frontend": {
      "panels": [
        {
          "id": "network-graph",
          "title": "Relationship Network",
          "location": "entity-sidebar",
          "entity_types": ["character", "faction", "location"],
          "url": "/panels/network.html",
          "icon": "Network"
        },
        {
          "id": "stats-tab",
          "title": "Statistics",
          "location": "entity-tab",
          "url": "/panels/stats.html"
        }
      ]
    },
    "settings": {
      "global": [
        {
          "key": "default_graph_depth",
          "label": "Default Graph Depth",
          "type": "number",
          "default": 3
        }
      ],
      "user": [
        {
          "key": "auto_expand",
          "label": "Auto-expand graph on load",
          "type": "boolean",
          "default": true
        }
      ]
    }
  },
  "permissions": ["read_entities"]
}

Manifest fields

FieldTypeRequiredDescription
idstringYesUnique id (lowercase, hyphens)
namestringYesDisplay name
versionstringYesSemver
descriptionstringYesBrief description
trust_tierstringYescommunity on the community registry; signed tiers only in studiobrain-plugins
author / license / icon / homepagestringNoMetadata
capabilities.backend.entrystringFor guestsWIT Component path (plugin.wasm)
capabilities.frontend.panelsarrayNoIframe panels
capabilities.settings.*arrayNoAdmin / user settings
permissionsarrayYes if you call the hostDeclared host capabilities

capabilities.backend.routes = routes.py is legacy. New plugins do not register Python files.

Panel locations

LocationRenders asDescription
entity-sidebarCollapsible sectionBeside the Visual Editor
entity-tabFull tabInjected into entity edit tabs
entity-footerBelow editorReserved

Component blocks

Layout Designer block id: plugin:{pluginId}:{panelId} (example plugin:my-analytics-plugin:network-graph). ComponentBlockRenderer delegates plugin:*:* to PluginBlockIframe.

Field widgets

Add field_widgets under capabilities.frontend:

"field_widgets": [
  {
    "id": "my-widget",
    "label": "My Widget",
    "category": "color",
    "accepts_options": true,
    "value_type": "string",
    "preview_url": "widgets/preview.png"
  }
]

Widget id becomes plugin:{pluginId}:{widgetId}. Sandboxed iframe (sandbox="allow-scripts").

DirectionMessageData
Host → Widgetsb-field-widget-update{ value, disabled, options }
Widget → Hostsb-field-widget-change{ value }
Widget → Hostsb-field-widget-resize{ height } (40–300px)
Widget → Hoststudiobrain-theme-requestRequest CSS variables
Host → Widgetstudiobrain-theme-response{ variables }

Tutorial: Building a Custom Field Widget.

Iframe sandbox

<iframe
  sandbox="allow-scripts allow-same-origin allow-forms"
  src="/api/plugins/panel/{pluginId}/{panelId}"
/>

Allowed: scripts, same-origin storage, forms. Blocked: top-level navigation, popups, parent DOM. The host injects CSP on /api/plugins/panel/{id}/{panel_id}.

PostMessage protocol

Types live in src/lib/plugin-message-protocol.ts. Full reference: Plugin Iframe Protocol.

Host → plugin

entity-context on load (entityType, entityId, data, theme). theme-change on light/dark. entity-updated after save or external edit.

Plugin → host

request-entity-data, entity-modified (field patch), navigate, toast, resize.

Validate with isPluginMessage / isHostMessage. The host checks event.origin. Panels should treat hostOrigin from entity-context as the only trusted parent.

Plugin theme compliance

Mandatory. Hardcoded colors break user themes.

StudioBrain has 12 semantic surfaces. Users recolor them in Settings. Hex values and Tailwind palette utilities (bg-blue-500) will look wrong.

Receiving theme

  1. entity-context includes the initial theme.
  2. theme-change fires on toggle.

Iframes do not inherit host CSS variables. Either apply a dark class from the message, or request variables (studiobrain-theme-request) and set --surface-* on your root.

.plugin-card {
  background-color: var(--surface-elevated-bg);
  color: var(--surface-elevated-text);
  border: 1px solid var(--surface-elevated-border);
}
/* WRONG */
.plugin-card { background-color: #1e293b; color: white; }
SurfaceUse for
baseMain backgrounds
elevatedCards, panels
primary / secondary / accentActions and highlights
success / warning / error / infoStatus

Lifecycle in the panel

On load, handle entity-context and render. On entity-updated, refresh. On theme-change, restyle. Report height with ResizeObserverresize.

Plugin state persistence

Use the plugin-data API (per plugin, per project):

const PLUGIN_ID = 'my-analytics-plugin';
const API_BASE = '/api/plugins';
 
await fetch(`${API_BASE}/${PLUGIN_ID}/data/analysis`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data)
});

record_type (analysis) is a collection name. Official proofs (entity-notes, entity-snapshots) use this instead of a sidecar DB.

Plugin settings

GET/PUT /api/plugins/{id}/settings
GET/PUT /api/plugins/{id}/settings/user

Read both from the panel and merge. The Component reads the same keys through WIT settings imports.

Backend guests

PathWhat runs
Desktop / mobileWIT Component in wasmtime (Cranelift / Pulley). Same bytes.
Official cloudWorker per plugin, compiled at our deploy.
Community cloudNo guest on Biloxi Cloudflare. Panel and/or author backend.

Hooks (on-entity-validate, create/update) are WIT exports the host calls. Official cloud maps the same contract onto the deployed Worker. There is no events.py.

network:local is official desktop-only.

Directory structure

my-analytics-plugin/
  plugin.json
  plugin.wasm              # WIT Component (desktop / mobile)
  src/lib.rs               # cargo-component guest
  frontend/
    network.html
    stats.html
  assets/
    icon.svg
    screenshot.png

Official universal plugins also have a Worker package that our deploy compiles. Community zips must not include a Worker we are expected to run.

Local development

  1. cargo component build --release and refresh plugin.wasm.
  2. Copy or symlink into the current project’s plugin dir.
  3. Enable for that project. Tenant allowlist still applies on cloud.
  4. Panel-only iteration can point the iframe at localhost; hot reload is the iframe fetching your dev server.

Zip / git install APIs are development conveniences. They are not a second store. Marketplace is the only store UI.

Dev upload (not Marketplace submission)

POST /api/plugins/upload accepts .zip / .tar.gz (50 MiB compressed / 100 MiB decompressed), strips a GitHub-zip prefix, validates plugin.json, and installs into _Plugins/{id}/ for the current project. Path traversal is rejected. This does not publish to the community registry.

Marketplace listing

  • Official / vendor code: PR to studiobrain-plugins. Cloud Worker ships only when we deploy it.
  • Community plugin: PR the plugins index on studiobrain-community. See Submitting.
  • Community data: catalog-data index on the same registry — not studiobrain-templates.
  • Official data: studiobrain-templates only.

Install is per-project. Tenant policy is an allowlist. CatalogSync does not already merge community indexes.

Browse uses GET /api/catalog?kind=plugins&source=official|community (and the Marketplace UI). Do not treat POST /api/marketplace/registry as the public submit path.

Next steps