Declaring what a plugin needs from the other tier
A plugin usually has a counterpart across the wire, and there are two strengths of that relationship: one it cannot work without, and one it does more with. Both are declared rather than discovered, so a host can draw the relationship before anything has loaded.
Frontend → backend
A required backend plugin gates rendering: while it is absent or switched off, the plugin's slot components do not render. An optional one never gates anything — reacting to it is the plugin's own job.
definePlugin({
name: '@app/notebook',
requiredBackendPlugins: ['kernels'], // no kernels, no notebook
optionalBackendPlugins: ['search'], // nicer with it, fine without
});
function NotebookToolbar() {
// A required plugin is guaranteed by the time this runs. An optional one
// is a question, and this is how it is asked.
const canSearch = useBackendPlugin('search');
return canSearch ? <SearchButton /> : null;
}
The predicate that answers "is this backend plugin available?" is the application's to supply, because only the application knows where its server is:
useReactor(reactor, { isBackendPluginAvailable });
Backend → frontend
The mirror image, on the PluginManifest:
PluginManifest(
name="checkout",
version="1.0.0",
frontend_dependencies=["@app/checkout"], # required
optional_frontend_dependencies=["@app/header"], # nice to have
)
Declared, not enforced — and why
Backend dependencies are checked at registration and refused outright. A
frontend dependency cannot be: the plugins live in a browser the platform cannot
see, and a backend that refused to start because nobody had opened a page yet
would be refusing for the wrong reason.
So the platform answers for a caller that can see both sides:
platform.frontend_requirements(["@app/checkout"])
# {"checkout": {"required": ["@app/checkout"], "optional": ["@app/header"],
# "missing_required": [], "missing_optional": ["@app/header"]}}
The same answer is served at GET /plugins/frontend-requirements?active=…, which
is how a frontend asks "is anything the server needs missing from what I
loaded?".
Seeing it work
In the music example, unchecking the Python
catalog plugin empties the store — both React plugins declare
requiredBackendPlugins: ['catalog'] — while unchecking playlist leaves its
card exactly as useful, because the frontend declared that one optional.
Activation follows, not just rendering
requiredBackendPlugins gates rendering: a slot component whose backend
plugin is switched off does not draw. On its own that leaves the plugin
activated, holding contributions backed by a server that is no longer
answering — the plugin list says it is on while nothing it offers works.
setBackendPlugins closes that:
await reactor.setBackendPlugins(['catalog', 'playlist']);
// → { deactivated: ['@app/shop', '@app/catalog'], activated: [] }
A plugin whose required backend plugin goes away is stood down, dependants first; when it returns, so is the plugin. What crosses the wire is deactivation, never disabling — a server must not be able to undo somebody's checkbox, so a plugin a person switched off stays off.
In React, one line wires it to a running server:
useBackendPluginStream('http://localhost:8799');
It reads GET /plugins/state and follows GET /events/stream. Two properties
worth knowing, both deliberate:
- A dropped connection is not a server saying no. The last known state is kept and nothing is torn down because the network blinked — the same reason a backend does not refuse to start when no browser has loaded yet.
- Polling is correct. The stream is an optimisation over the state endpoint, and the fallback is the same code path with a timer, so a deployment that cannot hold a connection open loses latency and nothing else.
Direction matters, and only one way propagates: server → browser. A browser closing a view is not a reason to stand a plugin down for every other browser the server is serving.