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 plain node:http handler on the shared control server. Throws if that method+path is already taken by another plugin.
  • registerStatic(prefix, dir) — serves files under dir for any request path starting with prefix. 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 data status renders. cpu (percent of one core) and memory (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 own onRestart hook (if any) and cascades to dependsOn dependents exactly as a watch-triggered restart would. Can take a while to resolve if onRestart keeps 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 a controlServerReady handler), also relayed straight to the terminal that ran braid start, not just daemon.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

EventPayloadFires 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.
daemonShutdownThe 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.