Architecture
How Saltshaker loads, runs, and constrains plugins—focusing on isolation boundaries and the exposed plugin API.
Runtime model
Saltshaker plugins are packaged as versioned artifacts and activated by the client at runtime. A plugin is provided a constrained API surface that enables plugin behavior without exposing unrestricted host access.
- Saltshaker reads a public plugin catalog (metadata only).
- The client downloads a plugin artifact for a specific version.
- The client verifies integrity using a published SHA-256 hash.
- The client installs and activates the plugin.
- The plugin interacts with Saltshaker only through the exposed plugin API.
Plugin registry and packaging
Saltshaker uses a public registry repository that contains a catalog of available plugins and links to their bundled release artifacts. The registry does not contain plugin source code.
- Catalog index lists plugins and points to a per-plugin manifest.
- Per-plugin manifests declare metadata, permissions, resources, settings, and required client version.
- Release artifacts are bundled by a GitHub Actions workflow and published as versioned downloads.
Plugin authors declare the repository and commit for a given version; artifact URL and hash are produced during the bundling workflow.
Isolation model
Plugins execute in a constrained runtime that exposes a limited set of capabilities via a single injected
API object (api). Plugins should assume they cannot access host internals directly; they must use
only documented API surfaces.
- Log messages through
api.log(...). - Listen to platform events through
api.on(event, handler). - Emit plugin events through
api.sendEvent(event, ...args). - Read declared resources via
api.host.filemethods (permission-gated). - Subscribe to declared Dolphin event streams via
api.host.dolphinmethods (permission-gated). - Read and react to plugin settings via
api.settings.
Permissions, resources, and settings
Plugin capabilities are declared up-front in the registry manifest. The client uses this declaration to decide what to expose to a plugin at runtime.
Declare which categories of host functionality the plugin needs (e.g. reading resources or subscribing to Dolphin events). Keep permissions minimal.
Declare named resources (e.g. a specific config file) that the plugin may read by ID. Plugins request
resources by resourceId, not by arbitrary filesystem path.
Declare settings keys (typed) that Saltshaker surfaces in UI. Plugins read via
api.settings.get and can subscribe via api.settings.onChange.
Event model
The plugin runtime uses an event bus to deliver platform events into plugins and to accept plugin outputs back into the platform.
Dolphin events are forwarded to plugins using a namespaced convention:
dolphin:GameStartdolphin:GameEnddolphin:Disconnecteddolphin:Error
Plugins typically emit events that Saltshaker uses to coordinate behavior across users:
connect(roomCode, uid)disconnect(roomCode, uid)
Lifecycle and cleanup
A plugin must implement an initialization hook and should implement cleanup. When a plugin is stopped or switched out, Saltshaker triggers disposal and expects the plugin to unsubscribe from upstream sources and release event handlers.
- Initialize via
onInit(api). - Dispose via
onDispose()(recommended). - Unsubscribe from any upstream subscriptions you created (e.g. Dolphin events).
- Call disposer functions returned from
api.onto remove listeners. - Best-effort: if the plugin maintains a “connected” state, emit a final
disconnectduring disposal.
Security notes
Saltshaker is designed to minimize the exposed surface area available to plugins. The runtime is capability-driven: plugins only receive access to functionality declared in their manifest.
- Install plugins only from sources you trust.
- Prefer plugins with minimal permissions.
- Verify artifacts by hash (Saltshaker does this as part of installation).
- Plugin authors should treat user data as sensitive and avoid collecting more than necessary.