Plugin docs
Plugin sync and export
How your plugins follow you to your other devices, and how to export and import a plugin set.
Your Cuttle plugins follow you to every device signed in to the same Cuttle Cloud account. Each device installs them locally; nothing derived (automations, pipelines, extracted files) is copied between devices. To move a plugin set between instances that are not linked (or keep a copy), export it to a file and import it: Export and import.
What syncs
One document per user in the cloud table public.user_plugin_state (cloud/supabase/migrations/0024_user_plugin_state.sql), owner-only: no org member, not even an admin, can read another user's list. Per plugin it holds:
| Field | Syncs | Notes |
|---|---|---|
| Plugin id, version, package SHA-256, origin | yes | origin file or index:<download url>; a Development installation (cuttle plugins dev, origin dev) never syncs |
| Enabled / disabled | yes | blocked, incompatible and missing_package are per device and never travel |
| Projects it is enabled in | yes | by project id; a project this device does not have is ignored here and kept for the others |
| Optional grants (on / off) | yes | except critical ones, below |
sessions.spawn limits | yes, when they deny Bash | narrower limits apply on the other device; wider ones never do |
Settings with scope user (the default) | yes | |
Settings with scope machine or project | no | "This device only" / per project |
| Secret settings | not in this document | user-scope secrets in the encrypted secrets store (see below) |
Critical grants (process.spawn, runners.contribute, sessions.spawn without a Bash deny) | no | only that the plugin asks for them; each device approves them itself |
| Which device runs its scheduled automations | yes | "Run here instead" on any device moves them for all |
| Uninstall | yes | as a tombstone, kept 90 days |
Host storage (cuttle.storage) | no | per device, like a cache; not exported either |
Organization installs do not sync: they live on the one multi-user server.
On the other device
- A new plugin is installed with the same projects, optional grants, session limits and settings, as you, through the same checks an install runs (plugin policy, engine range, name clashes). A refusal leaves it uninstalled there and is logged.
- Critical permissions are asked again. The plugin page shows Approve on this device with the permissions and an acknowledgement, like the install sheet did. Until you approve, the parts that need them stay off (without
process.spawnits tool servers are not added to sessions; withoutsessions.spawnits automations are paused, saying why); the rest of the plugin works. The Plugins list marks it Approve on this device. - The package is not here yet. A package from a plugin index is downloaded and must hash to the synced SHA-256, or it is refused. A package you imported from a file shows the plugin as Needs package with the version and SHA it waits for; import that same
.cuttlepluginon this device and it starts on its own. - Updates. A newer version applied on another device is applied here too when it asks for nothing new; if it asks for more, it waits here for your review, and the current version keeps running. An older version (or the same version with other contents) is only offered, never forced.
- Uninstall. Uninstalling on one device uninstalls everywhere, and on the other devices its automations and pipelines are always kept as yours, never silently removed. A notification says so (
plugin.uninstalled_elsewhere). - Scheduled automations fire on one device only: the one that installed the plugin until you choose Run here instead somewhere else. The others show "Runs on <device>".
Conflicts and offline
- Per plugin, the later change wins. Each entry carries the time of the change that wrote it. If two devices change the same plugin before either syncs, the later change replaces the earlier one entirely (for that plugin; other plugins are unaffected).
- Offline changes wait in the outbox and are sent when the device is back online; nothing is lost.
- First sync of a device merges: plugins installed on only one side are kept, one installed on both takes the newer.
- A change this device cannot apply (policy refused a grant, a project it does not have) is not pushed back as an edit, but the next change you make to that plugin here is what the other devices get.
How it works
apps/server/src/services/cloud-sync/user-plugins-sync.ts, following the favourites lane (user-prefs-sync.ts):
- Push. A user edit through
/api/plugins(install, enable, disable, projects, grants, session limits, settings, update, run here, uninstall) queues auser_pluginoutbox entry with the edit time. Nothing else queues: a change arriving from another device goes through the same services without passing the routes, so it never echoes back. The drain reads the document, applies the cloud entry instead if it is newer than the edit, and otherwise writes this device's entry withput_user_plugin_state, a compare-and-swap on the document version (retried when another device wrote first). - Pull. Rides the cloud-sync tick and a Realtime binding on
user_plugin_state. An entry applies when it changed since this device last synced it and has no unpushed local edit. Plugins installed before linking are pushed once. - Carry-over. A device pushes what it cannot express as it arrived: a project it does not have, a permission its older version does not declare, a version whose package it does not have.
- No-login desktop (
AUTH_MODE=none). The local operator syncs as the linked account, as favourites do. - Flag off (
CUTTLE_PLUGINS). Nothing is applied and pushes stay queued until it is back on.
Limits
- Secret settings on a no-login desktop. They are user-scope secrets, which the ciphertext secrets lane carries for signed-in accounts. It does not yet map the no-login operator to the linked account, so on the desktop app re-enter secret settings on each device.
- The whole entry for a plugin is last-writer-wins; two devices changing different fields of the same plugin at the same time keep only the later one.
- Tombstones are pruned after 90 days; a device offline longer keeps a plugin uninstalled elsewhere until you uninstall it there.
- Per-project setting overrides do not sync.
Export and import
A JSON file with your plugin set, for another instance, a fresh install or a backup. Plugins → ⋯ → Export plugin set (or Export with packages), or:
cuttle plugins export [-o file.json] [--with-packages] [--include-secrets]
cuttle plugins import file.json # dry run: what would happen
cuttle plugins import file.json --apply --yes-required [--as-exported] [--grant <permission>]… [--only <id>]…What the file holds
GET /api/plugins/export (?withPackages=1), your personal installations plus, for an admin, the organization's:
{ "format": "cuttle.plugins.export", "version": 1, "exportedAt": "…", "pluginApi": "1.0.0",
"plugins": [{ "id": "acme.suite", "version": "1.2.0", "sha256": "…", "origin": "file | index:<url>",
"scope": "user | org", "enabled": true,
"projects": [{ "id": "<project id>", "name": "Payments API", "settings": { "<project-scope key>": "…" } }],
"grants": [{ "permission": "events.read", "granted": true, "scope": null }],
"settings": { "<key>": "…" }, // user and machine scope; never secret settings
"package": "<base64>" }] } // --with-packages only- A plugin whose package is not on this device (installed by sync, still Needs package) is exported without
package. - Secret settings are never in this file.
--include-secretsalso writes<file>.secrets.json, a passphrase-sealed archive made by the sensitive export with only theplugin-secretskind — admin, a current authenticator code and the audit log, exactly ascuttle export-secrets(sensitive-export.md). Restore it on the target withcuttle import-secrets <file>.secrets.json --kinds plugin-secrets --apply(dry run without--apply). The web export never includes secrets.
Importing
Plugins → ⋯ → Import plugin set… or cuttle plugins import:
Dry run first.
POST /api/plugins/import-set/previewreads the file and answers, per plugin, what would happen; it installs nothing and grants nothing. A row is Ready to review, Already installed (left as it is), or can't be installed, with the reason:- Package tampered — the embedded package does not hash to the exported SHA-256 (it changed after export). Refused and never stored.
- Needs package — not in the file and not on this device. Import that
.cuttleplugin(the row's Import its file…) and the preview runs again. A package that came from a plugin index is downloaded again instead, and must hash to the exported SHA-256. - an organization plugin, for a non-admin.
- Projects are matched by id (the same on every device of one Cuttle Cloud account), then by name when exactly one project you can edit has it; anything else is left unassigned for you to pick.
- Each plugin goes through the install sheet. The permissions, projects and session limits it had are pre-selected, but nothing is granted until you review them and press Install (critical permissions still need the acknowledgement). Only then does
POST /api/plugins/import-set/installinstall it with your choices, restore its settings (values the new package no longer accepts are skipped and listed) and, if it was disabled, disable it.
The CLI is the same: --apply installs the rows marked install, and grants only what the command line names — --yes-required for required permissions, --as-exported for the optional permissions and session limits the file had, --grant for others.