Scopes and permissions
A plugin can do only what its manifest asks for. Flare shows the user every request before any plugin code runs, and refuses everything else.
How it works
A plugin runs in a sandbox with no Node, no file system, no network and no access to Flare's window. It never holds a connection, a password or a token.
To read or change data, a plugin calls ctx. Flare checks each call against what the user allowed, then runs it as if the user had asked.
Writes go through Flare's guarded write, the same path as the user's own edits:
- On a database marked production, Flare asks first, in a confirmation that names the plugin.
- The user's team roles apply.
- Activity records the change under the plugin's name, with undo where Flare can undo it exactly.
Flare lists a plugin's contributions and permissions from its manifest. It doesn't run any of the plugin's code to do that.
Ask for permissions
Add a permissions field to the manifest. Anything you leave out isn't granted. With no permissions at all, a plugin can format cells, add themes and act on the records it's handed.
{
"permissions": {
"data": "write",
"network": ["api.stripe.com"],
"clipboard": true,
"optional": ["data:write", "network:api.stripe.com"]
}
}| Field | Values | What it allows |
|---|---|---|
data | "none", "read" or "write" | read reads the open database. write also changes records. |
network | host names, up to 20 | The https hosts ctx.fetch may reach, such as api.stripe.com or *.example.com. |
clipboard | true or false | Copying text with ctx.copy. Nothing can read the clipboard. |
optional | scopes | Scopes the user can untick at install. See below. |
Scopes
Flare turns permissions into named scopes. data: "read" is data:read, and write adds data:write. Each host in network is a network:<host>. clipboard is clipboard:write.
The install dialog, Settings, Plugins, the store and the CLI all use these names and words:
| Scope | What the user reads | Risk |
|---|---|---|
data:read | Read the database you have open. Only the one open in Flare, only while it is, and only as far as your own access goes. | medium |
data:write | Change records in it. Each change is recorded in Activity with the plugin's name, and undone there where Flare can. | high |
network:<host> | Send and receive data from <host>. Only that host, over HTTPS. What it sends there is between you and the plugin's author. | high |
clipboard:write | Copy text to your clipboard. It can never read your clipboard. | low |
A person at Flare reviews any plugin with a high scope before the store lists it. See Publishing.
@flare/plugin exports the same helpers Flare uses. scopesOf(permissions) lists the scopes, describeScope(scope) returns the words above, and scopeRisk(scope) returns the risk.
const scopes = scopesOf({
data: 'read',
network: ['api.stripe.com'],
clipboard: false,
optional: [],
});
const words = scopes.map((scope) => describeScope(scope).title);
Make scopes optional
List the scopes your plugin can live without in permissions.optional. The install dialog shows them as ticked boxes. The user can untick any of them and still install the plugin.
{
"permissions": {
"data": "write",
"network": ["api.stripe.com"],
"optional": ["data:write", "network:api.stripe.com"]
},
"contributes": {
"widgets": [{ "id": "customers", "title": "New customers" }],
"rowActions": [{ "id": "refund", "title": "Refund in Stripe", "write": true }]
}
}Here the user can keep the widget and still stop the plugin from changing records or reaching Stripe. Flare refuses the manifest if:
- an optional scope isn't one the plugin asks for;
data:readis optional and the plugin has widgets, columns or result views, which need it.
Unticking data:read also removes data:write. A plugin can't change what it can't read.
Check what you were granted
ctx.granted holds what the user allowed. ctx.can(scope) checks one scope. Check before you offer something the user turned off:
definePlugin({
rowActions: {
refund: async ({ records, container }, ctx) => {
if (!ctx.can('data:write') || !ctx.can('network:api.stripe.com')) {
return {
message: 'Allow this plugin to change records and reach Stripe first',
};
}
// ...
},
},
});Components get the same from useFlare(), and it updates when the user changes their choices:
<script lang="ts" setup>
import { Button, useFlare } from '@flare/plugin/vue';
const flare = useFlare();
</script>
<template>
<Button v-if="flare.can('data:write')" size="sm">Archive</Button>
</template>ctx.granted is { scopes, production }. scopes is what's left after the user's choices. production says whether they allowed production.
Production is the user's switch
Production access isn't a scope. It's a switch the user sets in the install dialog, next to the scopes, and it starts off:
Also on production databases. Off unless you turn it on. Even then, every change on production asks you first, as your own do.
While it's off, every data call on a production database rejects with a PluginError whose code is PRODUCTION_DENIED. The plugin's widgets and columns there show that instead of data. Check ctx.granted.production, or catch the error, and tell the user:
if (ctx.target?.production && !ctx.granted.production) {
ctx.toast('Allow this plugin on production in Settings, Plugins', { tone: 'info' });
}What users see
Flare reads and checks the plugin without running it. Then the install dialog shows what the plugin adds, every scope in the words above, the optional ones as boxes, and the production switch. Nothing installs until the user allows it.
After that:
- The user can change their choices in Settings, Plugins. The plugin sees the change at once in
ctx.granted. - An update that asks for any new scope shows them in the install dialog, with what the user decided before folded away. An update that waits for them keeps the plugin turned off until they allow it. An update that asks for the same or less installs as usual.
- Store plugins are checked for updates when Settings, Plugins opens and once a day, and listed in its Updates tab.
- Turning a plugin off stops its frames. Uninstalling removes its files, storage and preferences.
Ask for as little as you can
Ask only for what your plugin can't work without, and make the rest optional. A plugin that only formats cells or copies what it's given asks for nothing, and the install dialog says so. That makes it an easy yes.
Hosts
- A host name such as
api.stripe.commatches only that name. *.example.commatches every name underexample.com, such asapi.example.comanda.b.example.com. It doesn't matchexample.comitself, so declare both if you need both.- Schemes, ports, paths, IP addresses and
localhostaren't allowed. Requests are always https.
The sandbox
Each plugin gets its own origin, flare-plugin://<id>/. Its code runs in frames on that origin inside Flare's window. One hidden logic frame runs its functions, and each component on screen gets a view frame. Neither has Node, a preload or window.flare.
Every file on the plugin's origin is served with this Content Security Policy:
default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:; font-src 'self' data:; connect-src 'none'; frame-src 'none';
worker-src 'none'; form-action 'none'; base-uri 'none'A plugin can't fetch, open sockets, navigate, open windows or load remote images. Remote images are blocked because a URL can smuggle data out. Flare also denies every permission request from a plugin origin, such as the camera, notifications or reading the clipboard. It blocks any attempt by a plugin frame to navigate away from its origin.
ctx.fetch is the only way out. Flare's main process makes the request, over https, to the hosts the plugin was granted. It checks the host again there and on every redirect. Requests carry no cookies. Responses are capped at 5MB, and requests time out after 30 seconds.
Frames talk to Flare only through postMessage. Flare answers a message only if it comes from a frame it made for that plugin, so one plugin can't pose as another. The messages are listed in The frame protocol.
Flare's servers see none of it
Plugins run on your computer. Data a plugin reads goes from Flare to the plugin's frame and stops there, unless the plugin sends it to a host you allowed.