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?
| Kind | Desktop | Mobile | Cloud |
|---|---|---|---|
| Official universal | WIT Component (wasmtime Cranelift) | Same Component (wasmtime Pulley, SBAI-7745) | Official Worker, compiled at our deploy. Panels on all surfaces. |
| Official desktop-only | Component + network:local | — | — |
| Community plugin | Component | Same Component | Panel iframe and/or the author’s backend. Never a Worker on BiloxiStudios Cloudflare. |
| Any data kind | Files in the project | Files in the project | Files 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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique id (lowercase, hyphens) |
name | string | Yes | Display name |
version | string | Yes | Semver |
description | string | Yes | Brief description |
trust_tier | string | Yes | community on the community registry; signed tiers only in studiobrain-plugins |
author / license / icon / homepage | string | No | Metadata |
capabilities.backend.entry | string | For guests | WIT Component path (plugin.wasm) |
capabilities.frontend.panels | array | No | Iframe panels |
capabilities.settings.* | array | No | Admin / user settings |
permissions | array | Yes if you call the host | Declared host capabilities |
capabilities.backend.routes = routes.py is legacy. New plugins do not register Python files.
Panel locations
| Location | Renders as | Description |
|---|---|---|
entity-sidebar | Collapsible section | Beside the Visual Editor |
entity-tab | Full tab | Injected into entity edit tabs |
entity-footer | Below editor | Reserved |
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").
| Direction | Message | Data |
|---|---|---|
| Host → Widget | sb-field-widget-update | { value, disabled, options } |
| Widget → Host | sb-field-widget-change | { value } |
| Widget → Host | sb-field-widget-resize | { height } (40–300px) |
| Widget → Host | studiobrain-theme-request | Request CSS variables |
| Host → Widget | studiobrain-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
entity-contextincludes the initial theme.theme-changefires 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; }| Surface | Use for |
|---|---|
base | Main backgrounds |
elevated | Cards, panels |
primary / secondary / accent | Actions and highlights |
success / warning / error / info | Status |
Lifecycle in the panel
On load, handle entity-context and render. On entity-updated, refresh. On theme-change, restyle. Report height with ResizeObserver → resize.
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/userRead both from the panel and merge. The Component reads the same keys through WIT settings imports.
Backend guests
| Path | What runs |
|---|---|
| Desktop / mobile | WIT Component in wasmtime (Cranelift / Pulley). Same bytes. |
| Official cloud | Worker per plugin, compiled at our deploy. |
| Community cloud | No 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.pngOfficial 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
cargo component build --releaseand refreshplugin.wasm.- Copy or symlink into the current project’s plugin dir.
- Enable for that project. Tenant allowlist still applies on cloud.
- 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-templatesonly.
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
- Getting Started
- Creating Plugins
- Examples — official proofs
- Plugin Iframe Protocol
- Community submissions