Plugins
start runs a loopback-only, token-guarded control server that both
braid's own built-in functionality and external plugins register routes,
static files, and process lifecycle listeners on.
Using plugins
List a plugin by package name/path in plugins — optionally as a [name, options] tuple:
export default defineConfig({
processes: [ /* ... */ ],
plugins: [
"@your-scope/braid-plugin-something",
["./plugins/local-plugin.js", { port: 4100 }],
],
});
No dynamic discovery — if it's not listed, it doesn't run. A plugin is
resolved from the config file's own location, so it's found in
your project's node_modules, not braid's. braid's own internal
core plugins (/api/status; per-process log persistence behind
braid logs; and the /api/processes/stop and
/api/processes/restart routes behind
braid stop <name>/restart <name>) aren't
configurable and aren't listed in plugins.
@aip-tech/braid-plugin-ui
is the first real external plugin built against this API — a web
dashboard with live status and stop/restart buttons.
Building a plugin
A plugin is an object matching BraidPlugin:
import type { BraidPlugin } from "@aip-tech/braid";
export const helloPlugin: BraidPlugin = {
name: "hello",
register(ctx, options) {
ctx.registerRoute("GET", "/hello", (req, res) => {
res.writeHead(200, { "content-type": "text/plain" });
res.end("hello from a braid plugin");
});
ctx.on("processCrash", (event) => {
ctx.log(`"${event.name}" crashed (code ${event.code})`);
});
},
};
export default helloPlugin;
register() can be async. A plugin that throws or rejects during
register() is logged and isolated — it doesn't take down
the manager or any other plugin. Runs in-process with full Node access, the
same trust level as any other dependency in your node_modules
— there's no sandboxing beyond that.
The PluginContext API
Passed to register(ctx, options), one instance per plugin:
registerRoute(method, path, handler)— a plainnode:httphandler on the shared control server. Throws if that method+path is already taken by another plugin.registerStatic(prefix, dir)— serves files underdirfor any request path starting withprefix. Throws if the prefix is already taken.registerUpgrade(path, handler)— raw HTTP Upgrade dispatch (e.g. for a WebSocket) on an exact path match.on(type, handler)— subscribes to a process lifecycle event, see below.getProcesses()— the current{ name, pid, alive, startedAt, cpu?, memory? }[]for every configured process, the same datastatusrenders.cpu(percent of one core) andmemory(RSS bytes) are absent until the daemon's first sample, or while a process is stopped.stopProcess(name)— stops one named process. Resolves"ok","unknown"(not configured, or not currently running), or"busy"(a restart is already in progress for it).restartProcess(name)— stops and respawns one named process, then runs its ownonRestarthook (if any) and cascades todependsOndependents exactly as a watch-triggered restart would. Can take a while to resolve ifonRestartkeeps retrying. Resolves"ok","unknown", or"busy".log(message)— writes a line to stderr, prefixed with this plugin's own name. If sent before the daemon's ready handshake (e.g. from acontrolServerReadyhandler), also relayed straight to the terminal that ranbraid start, not justdaemon.log.
stopProcess/restartProcess are available to
every plugin, not only ones that registered the affected process
themselves — a plugin can stop or restart any configured process. Same
trust level as any other capability in your node_modules, worth
knowing before wiring one up to something you didn't write yourself.
The control server itself is loopback-only (127.0.0.1) and
requires a bearer token on every request (a per-run secret written to the
pidfile) — enforced automatically for every route, a plugin's own
handler doesn't need to check it. A plain browser navigation can't send a
bearer header, so a one-time ?token= query param is also
accepted; see Serving browser content if
you're building something a browser loads directly.
Lifecycle events
| Event | Payload | Fires when |
|---|---|---|
processStart | { name, pid } | A process is first forked. |
processExit | { name, code, signal } | A process's underlying child exits. |
processCrash | { name, code } | A process crashes (or fails to start) — the whole stack is about to shut down. |
processRestart | { name } | A watch-triggered restart happened internally, not exited. A manual (restartProcess/braid restart <name>) or dependsOn-cascade restart shows up as a plain processExit+processStart pair instead, not this event. |
processOutput | { name, stream, chunk } | Raw stdout/stderr bytes from a process, already line-prefixed. |
daemonShutdown | — | The daemon is shutting down — the last chance to clean up (e.g. end open connections) before the control server closes. |
controlServerReady | { port, token } | Fires once, after the control server is listening and every plugin has finished register()'ing — the earliest point a plugin can know its own reachable port/token (register() itself runs before the port is known). Useful for logging a browser-openable URL for content served via registerStatic. |
Every listener is isolated: a synchronous throw or a rejected promise from one plugin's handler is logged, not left to crash the manager or block other plugins' listeners for the same event.
Serving browser content
The control server's bearer-token check runs on every request, including
static files — a plain browser navigation to
http://127.0.0.1:<port>/ can't send an
Authorization header, so it needs a different entry point than
an API client gets.
A one-time ?token=<token> query param is accepted as an
alternative to the header. The first request that authenticates this way
gets a session cookie (scoped to that control server's own port, since a
browser's cookie jar for 127.0.0.1 isn't itself port-scoped)
and, for a GET, a redirect that strips the token back off the visible URL.
From then on the browser's own fetch() calls authenticate via
that cookie automatically — no token needs to live in the page's own
JavaScript.
Putting this together, a plugin serving a browser-facing page typically
calls registerStatic during register(), then waits
for controlServerReady to log an open-this-URL line with the
token attached — see the
@aip-tech/braid-plugin-ui
source for a working example.