The frame protocol
You don't need this page to build a plugin. @flare/plugin handles the protocol for you. Read on if you're curious, or if you want to verify what a plugin can and can't do.
Frames
A plugin runs in frames on its own origin, flare-plugin://<id>/, or its dev server during development. They're sandboxed <iframe>s with no preload, so there's no Node and no window.flare. Every file on the origin is served with a Content Security Policy that blocks the network. See the sandbox.
There are two kinds of frame. The logic frame is hidden, and there's one for as long as the plugin is on. It runs setup and every function: cell formatters, columns, actions, commands and function result views. Each component on screen, whether a widget, a command view or a result view, gets its own view frame. Its props arrive in mount, and again in props whenever they change.
Both load Flare's frame page, __flare/frame.html?role=logic or ?role=view. It loads Flare's runtime, then the plugin's dist/plugin.css and dist/plugin.js, all from the plugin's origin:
| File | What it is |
|---|---|
__flare/frame.html | The page both frames load. No inline script. |
__flare/runtime.js | The guest: listens to Flare, loads the plugin, says ready. |
__flare/vue.js | Vue, which import ... from 'vue' in a built plugin becomes. |
__flare/sdk.js | @flare/plugin in a frame. |
__flare/sdk-vue.js | @flare/plugin/vue: the composables and Flare's components. |
__flare/runtime.css | The components' styles, the theme's tokens and Flare's faces. |
The frames' policy blocks inline scripts, so there's no import map. flare-plugin build writes these paths into the bundle instead, and every plugin shares one Vue and one set of components.
Messages
Frames talk to their parent with postMessage only. Flare answers a message only when its event.source is a frame it made for that plugin, so one plugin can't pose as another. It checks every call against the plugin's granted permissions. Every message carries flare: 1 (PROTOCOL), so Flare ignores stray messages from libraries or browser extensions.
if (isFrameMessage(event.data)) {
const message = event.data;
}From a frame to Flare
type | Fields | What it says |
|---|---|---|
ready | role, implements | The plugin has loaded. Lists the contributions definePlugin implements, so Flare knows which declared ones are missing and never calls them. |
api | id, method, args | A call to ctx. method is its path, such as data.read, and args are its arguments in order. |
result | id, ok, value or error | The answer to an invoke. |
log | level, values | A line for the plugin's log, at level log, info, warn or error, with the values as text. |
resize | height | A view's content height, so command and result views can grow to fit, up to a limit. |
crash | error | An uncaught error in either kind of frame. Shown on the view and in the plugin's log. |
From Flare to a frame
type | Fields | What it says |
|---|---|---|
init | theme, target, granted, preferences, development | Sent first, and again when any of it changes: every --flare-* theme variable and the mode, the open database, what the user allowed, the preferences, and whether it runs from a dev server. |
mount | kind, contribution, props | Mounts a view frame's component: widget, commandView or transform, by id, with its props. |
props | props | A mounted view's new props: a widget's changed settings or size, a refresh, new results. |
invoke | id, kind, contribution, input | Runs one of the plugin's functions. For cells, input is a batch of Cells. |
api-result | id, ok, value or error | The answer to an api call. |
Errors travel as WireError, { code, message, stack? }, and become a PluginError on the other side.
The api methods
Each ctx call is one method. Flare checks it against the plugin's permissions before doing anything:
method | Needs | How Flare answers |
|---|---|---|
data.containers, data.read, data.aggregate | data:read | Reads the open database through the dashboard's read path. |
data.query | data:read | The engine's query path, as the Query Editor runs it, refusing anything that writes. |
data.update, data.insert, data.remove | data:write | The engine's write path, through Flare's guarded write: a confirmation naming the plugin on production, team roles, and Activity with undo where possible. |
storage.get, storage.set, storage.remove, storage.keys | none | The plugin's own store, kept by Flare's main process, 5MB. |
fetch | network:<host> | Made by Flare's main process, https only, to a declared host, checked again there. |
copy | clipboard:write | Copies to the clipboard. |
toast, openUrl | none | Flare's toasts; a confirmation before opening an https link. |
A plugin's life
- Flare creates the logic frame. The runtime loads the plugin's code and sends
ready. - Flare replies with
init, and the logic frame runssetup(ctx). - As the user works, Flare sends
invokefor a batch of cells, a page of a column, an action or a command. The frame replies with aresult. Meanwhile the plugin's code sendsapicalls, each answered with anapi-result. - Each view on screen gets a view frame. It sends
readyand receivesinitandmount. It sendsresizewhen its content changes, and receivespropswhen its props change. - When the theme, preferences or permissions change, Flare sends
initagain to every frame.
Constants
From @flare/plugin/protocol:
| Export | Value |
|---|---|
PROTOCOL | 1 |
DEV_PORT | 5199, where flare-plugin dev serves and Flare looks. |
DEV_MANIFEST_PATH | /__flare/plugin.json, what a dev server answers with the package.json. |
FRAME_PAGE | __flare/frame.html |
RUNTIME_SCRIPT | __flare/runtime.js |
RUNTIME_STYLES | __flare/runtime.css |
pluginUrl(id, path) | A file's address on a plugin's origin: flare-plugin://<id>/<path>. |
isFrameMessage, isHostMessage | Whether a message is one of the protocol's. |