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.