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.

High-level flow
  1. Saltshaker reads a public plugin catalog (metadata only).
  2. The client downloads a plugin artifact for a specific version.
  3. The client verifies integrity using a published SHA-256 hash.
  4. The client installs and activates the plugin.
  5. 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.

What a plugin can do
  • 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.file methods (permission-gated).
  • Subscribe to declared Dolphin event streams via api.host.dolphin methods (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.

Permissions

Declare which categories of host functionality the plugin needs (e.g. reading resources or subscribing to Dolphin events). Keep permissions minimal.

Resources

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.

Settings

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.

Inbound events (examples)

Dolphin events are forwarded to plugins using a namespaced convention:

  • dolphin:GameStart
  • dolphin:GameEnd
  • dolphin:Disconnected
  • dolphin:Error
Outbound events (core contract)

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.

Lifecycle expectations
  • 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.on to remove listeners.
  • Best-effort: if the plugin maintains a “connected” state, emit a final disconnect during 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.