packages/docs/development/host-pattern.md
Nuclear exposes player functionality to plugins through the host pattern. Every feature area the plugin system supports follows the same three-layer structure:
Plugins call methods on an API class (e.g. api.Queue.addToQueue()), which holds a reference to the host and delegates to it.
packages/plugin-sdk/src/types/)A TypeScript type with no implementation. Defines exactly what the player must provide for a domain.
// types/queue.ts
export type QueueHost = {
getQueue(): Queue;
addToQueue(tracks: Track[]): void;
// ...
};
packages/plugin-sdk/src/api/)The surface plugins interact with. Holds an optional reference to the host and guards every call with #withHost, which throws if the host isn't present (e.g. in a test environment where no player is running).
// api/queue.ts
export class QueueAPI {
#host?: QueueHost;
constructor(host?: QueueHost) {
this.#host = host;
}
#withHost<T>(fn: (host: QueueHost) => T): T {
const host = this.#host;
if (!host) {
throw new Error('Queue host not available');
}
return fn(host);
}
addToQueue(tracks: Track[]) {
return this.#withHost((host) => host.addToQueue(tracks));
}
}
All API classes are assembled into NuclearAPI in packages/plugin-sdk/src/api/index.ts, which is what plugins receive as their api object.
packages/player/src/services/)Lives in the player. Implements the host interface and bridges it to whatever backs the domain - a Zustand store, a provider registry, a Tauri API, etc.
// services/queueHost.ts
import type { QueueHost } from '@nuclearplayer/plugin-sdk';
import { useQueueStore } from '../stores/queueStore';
export const createQueueHost = (): QueueHost => ({
getQueue: () => useQueueStore.getState().queue,
addToQueue: (tracks) => useQueueStore.getState().addToQueue(tracks),
// ...
});
export const queueHost = createQueueHost();
The singleton is passed into NuclearPluginAPI by createPluginAPI (packages/player/src/services/plugins/createPluginAPI.ts) when a plugin loads.
| Domain | Backed by |
|---|---|
| Queue, Settings, Favorites | Zustand store |
| Metadata, Streaming, Dashboard | Plugin provider registry and optional store |
| HTTP | Fetch wrapper (implemented in Rust) |
| Shell | Tauri shell API |
| Logger | Scoped logger |
Provider-backed hosts resolve which registered provider to call and check capabilities where the domain supports them. See the Providers doc for how providers register and what kinds exist.
When a plugin calls api.Metadata.search({ query: 'Radiohead' }):
api.Metadata.search(params)MetadataAPI.#withHost checks host is present, calls metadataHost.search(params)metadataHost resolves the active metadata provider from the registrymetadataHost calls provider.searchArtists(params) (or whatever the provider implements)ArtistRef[]metadataHost returns SearchResults to the pluginpackages/plugin-sdk/src/types/yourDomain.tspackages/plugin-sdk/src/api/yourDomain.ts - wraps the host with #withHostNuclearAPI in packages/plugin-sdk/src/api/index.tspackages/player/src/services/yourDomainHost.tsNuclearPluginAPI in createPluginAPI.ts