Synapse Framework
Themes Plugins Docs Home Github
Github - Releases Join Discord

Synapse Framework — Plugin system

Plugins are unsandboxed JavaScript modules that extend the desktop app: UI overlays, routes, editor hooks, bridge providers, filesystem access, and more. They load app-wide on the main webview only — undocked V3 tabs and Script Hub secondary webviews skip plugin bootstrap.

This guide covers the generic plugin API. Vendor-specific bridge plugins (Cosmic API, Velocity API, etc.) are not documented here.


What plugins are

PropertyDetail
RuntimeJavaScript executed in the main Tauri webview via pluginCodeRunner.ts
BootstrapGlobalPluginProvider.tsx calls pluginHost.bootstrap() once on the main window
Secondary webviewsisSecondaryBridgeWebview() → no bootstrap; bridge relay only
Lifecycleactivate(host) on enable; deactivate(host) on disable or framework restart
StorageOn-disk folder under app data; enabled list in localStorage
flowchart TB
  ZIP["Install ZIP"] --> Disk["plugins/{id}/"]
  Disk --> Registry["localStorage registry"]
  Registry --> Bootstrap["pluginHost.bootstrap()"]
  Bootstrap --> Activate["activate(host)"]
  Activate --> UI["Overlays / routes / hooks"]
  Activate --> Bridge["Optional bridge provider"]

On-disk layout

Plugins live under the Tauri app data directory:

{app_data}/plugins/{plugin_id}/
  plugin.synapse-plugin    # Required — manifest + JS body
  bin/                     # Optional — exe, dll, assets
  ...
  • Root path: resolved by plugins_root() in src-tauri/src/plugins.rs
  • Plugin IDs: alphanumeric, -, _ only
  • Multi-file packs: any relative path under the plugin folder (ZIP install extracts the full tree)
  • Path traversal (..) is rejected by plugins_resolve_path

File format

Each plugin is a single .synapse-plugin file (or plugin.synapse-plugin in a pack):

  1. Manifest block — JSON inside a comment header
  2. JavaScript body — plugin code with activate / deactivate exports
/* @synapse-plugin-manifest
{
  "format": "synapse-plugin",
  "formatVersion": 1,
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "Optional description",
  "permissions": {
    "ui": true,
    "nav": true
  }
}
*/

function activate(host) {
  host.log("activated");
}

function deactivate(host) {
  host.log("deactivated");
}

exports.activate = activate;
exports.deactivate = deactivate;

Parsing and serialization: src/app/synapse-v3/plugins/pluginFileFormat.ts


Registry

Enabled plugins and load order are stored in localStorage:

Keysynapse.v3PluginsRegistry.v1
enabledIdsPlugins to load on bootstrap
orderDisplay / load order
lastAppliedAtTimestamp after Save + restart

Enable / disable in the Plugins UI (setPluginEnabled in v3PluginRegistry.ts). Disabling a plugin writes a tombstone on disk (plugins_tombstone) so it does not reload until re-enabled.

Structural changes (new permissions, new routes, pack file updates) require Save and Restart Framework (host.restartFramework() or the Plugins page button).


Lifecycle

  1. Installplugins_install_zip extracts to plugins/{id}/
  2. Enable — add to enabledIds in registry
  3. Save — persist registry; may prompt restart
  4. Restart Framework — reload webview; pluginHost.bootstrap() runs activate() for each enabled plugin
  5. Disabledeactivate(), tombstone, remove from enabledIds
  6. Deleteplugins_delete removes folder and registry entry

Permissions

Declared in the manifest permissions object. Each key gates a group of host APIs. full: true grants everything.

PermissionGates
tauriAllowlisted invokeTauri commands (see below)
fshost.fs.*, readPluginFile, writePluginFile, resolvePluginPath
httphost.http() — backend fetch, bypasses webview CORS
processhost.exec, spawnPluginProcess, pluginProcessRequest, killPluginProcess, isPluginProcessAlive, findProcessByName
bridgeBridge hooks, providers, executeBridge, getBridgeStatus, emitBridgeLog, disablePlugin
airegisterAiTool, registerPluginsAiTool
themepatchV3Theme, patchShellTheme, patchOgTheme, patchSynapseXTheme, applyV3Theme
editoronEditorTabOpen, onEditorContentChange, registerEditorAction
navnavigate, navigateV3Page, registerV3Page, hideNavItem, showNavItem
routesregisterRoute, registerGlobalRoute
storagegetPluginStorage, setPluginStorage, dataDir, storageFile
uimount, unmount, registerOverlay, registerShellOverlay, injectGlobalStyle
settingsgetAppSettings, patchAppSettings, setUiMode, restartFramework
fullAll of the above + unrestricted invokeTauri

Source: src/app/synapse-v3/plugins/v3PluginTypes.ts


Host API reference

The host object passed to activate(host) is built in PluginHost.createHostApi(). Grouped by concern:

MethodPermissionDescription
mount(target, node, options?)ui / fullRender React/DOM/HTML into any live selector; returns disposer
unmount(key)ui / fullRemove a prior mount
registerOverlay(slot, component)ui / fullRegister a React overlay in a UI slot
registerShellOverlay(shell, component)ui / fullOverlay mapped to a shell (blue, synapseV3, etc.)
injectGlobalStyle(id, css)ui / fullInject global CSS; can auto-hide theme nav
hideNavItem / showNavItem / isNavHiddenui / fullHide shell nav entries
navigate(path, options?)nav / fullReact Router navigation
setUiMode(mode, options?)settings / fullSwitch shell + optional reload
registerV3Page({ id, label, render })nav / fullAdd a V3 top-bar page
navigateV3Page(page)nav / fullSwitch V3 page
registerRoute / registerGlobalRouteroutes / fullAdd React Router routes
MethodPermissionDescription
host.fs.read/write/remove/mkdir/exists/listfs / fullGeneric backend filesystem (any path)
readPluginFile(relativePath?)Read from plugin folder (default: plugin.synapse-plugin)
writePluginFile(relativePath, contents)fs / fullWrite into plugin folder
resolvePluginPath(relativePath)fs / fullAbsolute path inside plugin dir
http(request)http / full{ url, method?, headers?, body?, timeoutMs? }{ status, ok, headers, body }
MethodPermissionDescription
exec(request)process / fullOne-shot { program, args?, cwd?, stdin? }
spawnPluginProcess(relativeExe, args?)process / fullLong-lived process from plugin folder
pluginProcessRequest(handle, request, timeoutMs?)process / fullJSON IPC to spawned process
killPluginProcess(handle)process / fullTerminate spawned process
isPluginProcessAlive(handle)process / fullCheck if handle is running
findProcessByName(name)process / fullOS process search (PIDs)
MethodPermissionDescription
getPluginStorage(key) / setPluginStorage(key, value)storage / fulllocalStorage prefixed per plugin
dataDir()storage / fullPlugin-specific data directory path
storageFile(name)storage / full{ read(), write(contents) } for a file in data dir
MethodPermissionDescription
getAppSettings() / patchAppSettings(partial)settings / fullRead/write app settings
getV3Theme, patchV3Theme, applyV3Themetheme / fullV3 theme
getShellTheme, patchShellThemetheme / fullDefault shell theme
getOgTheme, patchOgThemetheme / fullSynapse Original theme
getSynapseXTheme, patchSynapseXThemetheme / fullSynapse X theme
MethodPermissionDescription
onEditorTabOpen(fn)editor / fullTab opened hook
onEditorContentChange(fn)editor / fullContent changed hook
registerEditorAction({ id, label, run })editor / fullEditor toolbar action
registerAiTool / registerPluginsAiToolai / fullRegister AI function tools

Plugins with bridge permission can observe, wrap, or replace legacy Port/Stream/Compat execute.

MethodDescription
onBridgeExecute(fn)Called before each execute
wrapBridgeExecute(fn)Transform or cancel execute ({ cancel, source })
registerBridgeProvider({ id, label, attach?, getStatus?, execute, detach?, native? })Register a custom executor backend
unregisterBridgeProvider(id)Remove provider; re-enables legacy if this plugin owned it
setLegacyBridgeDisabled(disabled)When true, blocks Port/Stream/Compat in Rust
isLegacyBridgeDisabled()Read legacy-disabled flag
executeBridge(source, method?)Dispatch execute via bridgeDispatch.ts
getBridgeStatus()Tauri bridge_status snapshot
emitBridgeLog({ level, message })Push line to F9 console
requestBridgeStatusRefresh()Force immediate provider status poll
disablePlugin()Tombstone + remove from registry (recovery)

Ownership model: When a provider calls setLegacyBridgeDisabled(true), only that provider’s execute runs. Undocked V3 tabs relay provider execute to the main window. Unregistering a provider calls bridge_set_legacy_enabled to restore Port/Stream/Compat.

See EXECUTOR_BRIDGE.md for legacy bridge methods.

MethodPermissionDescription
restartFramework()settings / fullReload app webview
restartFrameworkAsAdmin()settings / fullElevated restart
isRunningElevated()Check admin elevation
reloadApp()Alias for restartFramework()
invokeTauri(cmd, args?)tauri / fullDirect Tauri invoke (allowlisted unless full)
log(...args)Console + plugin loader log

Tauri commands

Plugin management (permissions.tauri)

CommandPurpose
plugins_get_rootApp data plugins directory
plugins_listList installed plugins + manifests
plugins_read_fileRead file from plugin folder
plugins_write_fileWrite file to plugin folder
plugins_deleteDelete plugin folder
plugins_tombstoneMark plugin disabled on disk
plugins_install_zipInstall from ZIP
plugins_export_zipExport plugin as ZIP
plugins_scaffoldCreate blank plugin template
plugins_resolve_pathResolve relative path in plugin dir

Process and data

CommandPurpose
plugin_process_spawnSpawn long-lived process
plugin_process_requestJSON request to spawned process
plugin_process_killKill spawned process
plugin_process_aliveCheck process alive
plugin_process_find_by_nameFind PIDs by name
plugin_data_dirPer-plugin data directory
plugin_fs_read/write/remove/mkdir/exists/list/searchBackend filesystem
plugin_http_fetchBackend HTTP
plugin_process_execOne-shot process exec

Bridge-related (allowlisted)

CommandPurpose
bridge_statusBridge connection snapshot
bridge_send_executeQueue script for legacy bridge
bridge_set_legacy_enabledEnable/disable legacy Port/Stream/Compat

Framework

CommandPurpose
restart_frameworkReload webview
restart_framework_as_adminElevated reload
is_running_elevatedAdmin check

Full-power extras (permissions.full)

CommandPurpose
send_ai_chatAI chat
send_ai_chat_streamStreaming AI chat

Full list: PLUGIN_TAURI_COMMANDS and PLUGIN_TAURI_FULL_EXTRA in src/app/synapse-v3/plugins/pluginHostConstants.ts


UI overlay slots

Register components with host.registerOverlay(slot, Component):

SlotTypical use
appApp-wide overlay
blueDefault Synapse Blue shell
synapseOriginalSynapse Original 2017
synapseXSynapse X shell
synapseV3Synapse V3 shell
shellGeneric shell wrapper
editorEditor chrome
topBarTop bar area
pluginsPagePlugins settings page
actionBarEditor action bar
mainLayoutSidebarMain layout sidebar
initScreenLoading / init screen

Source: PLUGIN_UI_SLOTS in pluginHostConstants.ts


Multi-file packs and subprocesses

Packs: Install a ZIP containing plugin.synapse-plugin plus binaries, DLLs, configs, etc. Use host.resolvePluginPath("bin/my-tool.exe") to get absolute paths.

Subprocesses: spawnPluginProcess starts a singleton host process per plugin (src-tauri/src/plugin_process_host.rs). Communicate via pluginProcessRequest with JSON payloads. The Rust side enforces one process per plugin ID and cleans up on kill.

Typical pattern:

const { handle } = await host.spawnPluginProcess("bin/helper.exe", ["--mode", "server"]);
const result = await host.pluginProcessRequest(handle, { action: "ping" }, 5000);

Troubleshooting

SymptomLikely causeFix
permission required in consoleManifest missing permissionAdd permission key, Save, Restart Framework
Plugin not loading after installNot enabled or tombstonedEnable in Plugins UI; check tombstone file
Overlay not visibleWrong slot or selectorVerify PLUGIN_UI_SLOTS; use mount with a valid CSS selector
mount target not foundDOM not readyDefer mount until after navigation or use registerOverlay
Bridge execute failsProvider owns bridgeCheck legacy_disabled in bridge_status; use provider or disable provider
Changes not appliedStructural change without restartSave + Restart Framework
Command not allowed on invokeNot in allowlistAdd tauri or full permission
CSS not applyingSpecificityUse injectGlobalStyle with unique id; check shell-specific selectors

Selector tips: Prefer data-* attributes on stable shell elements. Test in each uiMode you support. Use hideNavItem instead of fragile CSS when hiding nav entries.

Key source files

ConcernPath
Host APIsrc/app/synapse-v3/plugins/PluginHost.ts
Code runnersrc/app/synapse-v3/plugins/pluginCodeRunner.ts
File formatsrc/app/synapse-v3/plugins/pluginFileFormat.ts
Registrysrc/app/synapse-v3/plugins/v3PluginRegistry.ts
Tauri allowlistsrc/app/synapse-v3/plugins/pluginHostConstants.ts
Global bootstrapsrc/app/plugins/GlobalPluginProvider.tsx
Rust plugin I/Osrc-tauri/src/plugins.rs
Plugin backend FS/HTTPsrc-tauri/src/plugin_backend.rs
Subprocess hostsrc-tauri/src/plugin_process_host.rs

Related documentation