6.5 KiB
Plugins Domain
Owns the client-only plugin runtime foundation: manifest validation, deterministic load ordering, registry state, local manifest discovery, capability grants, browser-imported client entrypoints, disposable UI extension registries, plugin logs, and typed access to signal-server plugin support metadata.
The signal server stores plugin install metadata and event definitions, but it must never execute plugin code or store arbitrary plugin data. Executable plugin loading belongs to the renderer/Electron boundary and should enter this domain through PluginHostService.
Desktop local plugins are discovered from the Electron app data plugins folder. Discovery reads toju-plugin.json or plugin.json from immediate child folders and resolves declared entrypoint/readme paths only when they stay inside that plugin folder.
The standalone plugin store is available from the title bar Plugins button, the title-bar Plugin Store menu item, the legacy Settings page button, and the Plugin Manager header. It owns source manifest management, search, readmes, install/update/uninstall actions, and links back to installed-plugin management. Manifest kind describes runtime shape (client or library), while top-level manifest scope describes installation scope: omit it or use scope: "client" for global client plugins, and use scope: "server" for chat-server plugins. Server-scoped store entries are presented as Install to Server, Update Server, or Remove from Server. Server plugin downloads are user-local and server-specific: a server can publish requirement metadata, but each account must consent before those plugins are downloaded or activated on join. Members who are already in a server see new required plugin requirements as a blocking prompt with Install plugins or Leave server actions; new optional or recommended requirements appear as a title-bar banner that can be installed, rejected for the current session, or hidden for that server/plugin requirement version.
The plugin manager UI is split between Settings -> Client plugins for global client plugins and Settings -> Server -> Server plugins for chat-server plugins. The two pages filter by manifest scope and include installed plugins, capability grant toggles, per-plugin activate/reload/unload actions, runtime logs, extension-point counts, server requirements, generated settings, and docs.
The Store tab consumes user-managed HTTP(S), file://, or absolute local-path source manifests. Local-path sources and entrypoints are read through the Electron desktop file bridge. A source manifest can expose a plugins array whose entries include id, title, description, version, scope, author/authors, image/imageUrl, github/githubUrl, install/installUrl/manifestUrl, bundle/bundleUrl, and readme/readmeUrl. Installing a scope: "server" plugin fetches the linked plugin manifest, validates it, registers it with the client registry, and persists the basic install metadata as a server plugin requirement. When a different user joins that server, required plugins block the join until the user accepts the download; optional and recommended plugins are offered as selectable downloads and can be skipped. Once a server has local server-scoped plugins installed, the title bar shows a compact Server plugins button for that server. Installing a scope: "client" plugin persists it locally for the current desktop/browser client.
Store plugins can be published as cached browser bundles by adding bundle or bundleUrl to the source manifest entry. The bundle is a browser-safe ESM JavaScript file. During install, Electron downloads the bundle into app data under plugin-bundles/<plugin-id>/<version>/main.js, writes a cached manifest next to it, and registers the plugin from that local cached manifest path. If no bundle URL is provided and the manifest entrypoint is a relative browser module, Electron caches that entrypoint path instead. Browser-only clients still load directly from the source URL. Saved store sources refresh during app bootstrap; when a source advertises a higher version for an installed plugin, the store attempts to update the local cached bundle and persisted install metadata automatically.
The server-side plugin support API is metadata-only. The signal server can keep plugin id, requirement status, version range, install/source URLs, and the validated manifest snapshot needed for member clients to install required plugins. Plugin serverData API calls are handled as local per-user/per-server client state; HTTP plugin data persistence on the signal server returns PLUGIN_DATA_DISABLED.
Plugin data that belongs to the current client uses the Electron database when the desktop bridge is available. The plugin runtime writes api.clientData.* and api.serverData.* records to Electron's dedicated user-scoped plugin_data table, with renderer localStorage as the browser fallback. The legacy synchronous api.storage.* surface remains local and mirrors writes to the same Electron table when possible; plugins that need guaranteed database reads should use the async api.clientData.* methods.
Plugins can communicate over a plugin-only message bus through api.messageBus. It sends plugin-message-bus data-channel events that are ignored by the normal chat message reducers/effects, can target a peer or broadcast to connected users, and can include a bounded latest-message snapshot filtered by channel, timestamp, and deletion state.
Desktop plugin preferences that belong to the local user, including capability grants, disabled plugin ids, and previously activated plugin ids, are persisted through Electron's local database meta table with renderer localStorage as the browser fallback.
Runtime activation is explicit. PluginHostService.activateReadyPlugins() imports browser-safe plugin entrypoints from URL-resolvable manifests, passes a frozen TojuClientPluginApi, runs activate, then runs ready after the load-order pass. Successfully activated plugin ids are remembered locally, and store-installed plugins are reactivated for the active server when their persisted manifests load again. deactivate runs during unload/reload, disposables are cleaned in reverse order, and UI contributions are removed by plugin id.
Plugins that need fully custom UI can call api.ui.mountElement(id, { target, element, position }) with the ui.dom capability. The runtime tags mounted elements with plugin ownership metadata, replaces duplicate mounts for the same plugin/id pair, and removes remaining mounted elements when the plugin is unloaded.