Guides

Plugins

Every running stack has a small local HTTP server. Plugins add routes, pages and event listeners to it; braid's own logs and process controls are built the same way.

Using a plugin

List it in plugins, by package name or path, optionally with options:

braid.config.ts
export default defineConfig({
	processes: [/* ... */],
	plugins: [
		"@aip-tech/braid-plugin-ui",
		["./plugins/notify.js", { channel: "#dev" }],
	],
});

Plugins resolve from the config file's directory, so they come from your project's node_modules. Only listed plugins run.

Plugins run inside braid with full access to Node and to every process in the stack. Use ones you trust, as with any dependency.

Writing a plugin

A plugin is an object with a name and a register function:

plugins/notify.ts
import type { BraidPlugin } from "@aip-tech/braid";

const notify: BraidPlugin = {
	name: "notify",
	register(ctx, options) {
		ctx.on("processCrash", (event) => {
			ctx.log(`${event.name} crashed (exit code ${event.code})`);
		});

		ctx.registerRoute("GET", "/api/notify/ping", (_req, res) => {
			res.writeHead(200, { "content-type": "text/plain" }).end("pong");
		});
	},
};

export default notify;

register can be async. If it, or any route or event handler, throws, braid logs the error and keeps running. Every route is behind the stack's access token automatically. The full API is in the Plugin API reference.

Options cross to a background daemon as a structured-clone copy, so they must be plain data. A function in plugin options makes braid start fail with a clear error.

Serving a page to the browser

ctx.registerStatic(prefix, dir) serves a directory. Static files are public, since a page can't attach a token to its own script loads, so keep secrets out of them. API routes stay protected.

  1. Print the page's link with the token in the fragment

    ctx.on("controlServerReady", ({ port, token }) => {
    	ctx.log(`open http://127.0.0.1:${port}/#token=${token}`);
    });

    Browsers never send the part after # to a server, so the token stays out of logs.

  2. Read it once, then send it as a header

    const token = new URLSearchParams(location.hash.slice(1)).get("token");
    history.replaceState(null, "", location.pathname);
    
    await fetch("/api/status", {
    	headers: { Authorization: `Bearer ${token}` },
    });
  3. For a WebSocket, use a ticket

    A browser can't put headers on a WebSocket. POST /api/ws-ticket returns a single-use ticket, valid for 30 seconds, to connect with ?ticket=<ticket>.

The web dashboard's source is a complete working example.