ADR-0003: Web admin UI as a plugin
- Status: Accepted
- Date: 2026-05-08
- Deciders: Mesh-America
Context
The BBS will ship a web admin interface for sysop maintenance. The question is whether it's:
a) A first-class feature of the host binary, hard-wired into the architecture b) An optional module enabled via cargo features, but otherwise integrated like any other piece of the host c) A plugin against the same bbs-plugin-api that third-party transports and extensions use
mesh-citadel took the "first-class" path: the web UI was wired directly into the transport manager and shared significant code with the BBS core. This worked but it meant the plugin contract was theoretical - there was no real example of an extension built against it, so the contract drifted from what extensions would actually need.
Decision
The web admin UI ships as plugin (option c). It implements the same TransportEngine trait as bbs-cli and bbs-mesh, and uses the same Host interface.
The plugin lives in crates/bbs-web/. It's enabled via the admin-web cargo feature. Default: off.
Alternatives considered
Option (a) - first-class
Rejected. The plugin contract becomes vapor if there's no non-trivial example. We'd have a Plugin trait that no real plugin uses, and six months in, the trait would be subtly wrong for the first extension someone wrote.
Option (b) - opt-in but integrated
Rejected for the same reason. "Optional" doesn't validate the contract; "implemented against the contract" does.
Consequences
Positive
- The plugin API is forced to be expressive enough to support a real, complex feature: HTTP routes, static-file mounts, authentication integration, event subscriptions, config validation, lifecycle hooks. If
bbs-webcan be a plugin, almost any plausible extension can be. - Default-off is enforced naturally. Cargo features mean operators who don't want the web UI don't even compile it in. Smaller binary, less attack surface.
- The web UI doesn't get special privileges. It can't bypass permission checks, can't access the DB directly, can't read session tokens it shouldn't see - because it has the same
Hostinterface as a third-party plugin would. - Third-party UI projects (mobile app, terminal client, alternative web frontend) consume the same OpenAPI surface the web admin uses. The reference implementation is right there.
Negative
- Slightly more complex plugin API. The trait has to support static-file mounts and
axum::Routercontributions, not just raw command processing. This is the cost of forcing the API to be real. - Boundary discipline required. The web plugin must not reach into
bbs-coreprivate APIs as a shortcut. Code review enforces this; rustdoc visibility (pub(crate)vs.pub) helps. - Slower iteration in some cases. A change that affects both
bbs-coreandbbs-webis a coordinated change across two crates rather than one. In practice this is fine - small, stable interfaces don't require coordinated changes often.
Neutral
- The web plugin can declare dependencies on other plugins. For example, certain admin features may require the mesh transport to be loaded (e.g., "send a test mesh DM"). The supervisor resolves these at startup and refuses to start if dependencies aren't satisfied.
Notes
This decision validates by construction. The first time we find something the web plugin needs and the plugin API doesn't support, that's a signal to extend the API - and that extension benefits every other plugin.