0012 — Menus and config files are two front-ends to one AdapterManager

Status: Accepted (built — 2026-07-29 note below) Date: 2026-06-29

Context

An operator needs to plug in I/O endpoints and consoles/surfaces (a PipeWire device, a REAC stagebox, an X32, an MCU) two ways: from a GUI menu at the gig ("found a stagebox — add it"), and from a config file checked into the rig setup ("these are my endpoints, start them on boot"). The dangerous design is to build those as two separate code paths that can disagree — the menu adds an adapter the file does not know about, or editing the file does something subtly different from using the menu.

Decision

One AdapterManager is the single source of truth, and menus and files are two front-ends to it and to the same persisted config.

  • On start, the manager reads config/adapters.{yaml,json}, instantiates each enabled adapter via a type → factory registry, and tracks status.
  • The web UI add/edit/remove menu calls WS verbs (adapter.add/update/remove/list/types); the manager applies the change and persists it back to the same config file.
  • Editing the file and restarting yields the identical end state. There is no second code path.
  • adapter.types returns a per-type settingsSchema so the menu form is rendered generically — the same metadata→widget idea as the plugin editor (0010), so there is no per-adapter-type UI code.
  • Discovery feeds the same manager: a found device → discovery.addAsAdapter → AdapterManager.add → persisted → started. Same end state as hand-editing the file.

Today the server loads a single adapter chosen by environment variables (OPENMIXER_ADAPTER), via a lazy package loader. The multi-adapter AdapterManager, the config file, and the adapter.* / discovery.* WS verbs are designed and not yet built.

Consequences

  • The menu and the file can never drift, because they are the same state and the same persistence — "no second code path" is the whole point.
  • Reproducible rigs: the gig setup is a file you can version, diff, and restore; the menu is just a live editor for it.
  • A new adapter type is a factory registration plus a settings schema; the menu form and the file format both pick it up with no UI work.
  • Probes must degrade (a missing NIC disables that probe with a logged reason, never crashes discovery), so the manager always starts even on a partial rig.

Note — 2026-07-29: built, all three parts

The Context paragraph "The multi-adapter AdapterManager, the config file, and the adapter.* / discovery.* WS verbs are designed and not yet built" is no longer true of any of the three, though the WS verbs it names were later deleted outright rather than built — the REST entity map replaced them:

  • AdapterManager — the class in packages/server/src/adapter-manager.ts.
  • the config file — startConsole in packages/server/src/console-rig.ts resolves OPENMIXER_ADAPTERS_CONFIG or <stateDir>/adapters.yaml; an example ships at packages/server/config/adapters.example.yaml.
  • the verbs — buildAdapterAdminRows (packages/server/src/adapter-admin-rows.ts) and buildDiscoveryRows (packages/server/src/discovery-rows.ts) build the /adapter/* and /discovery/* REST rows; packages/server/src/server.ts is what registers them into the resource registry.

The per-type settingsSchema this ADR predicted is real too (xtouchRegistration in packages/adapter-xtouch/src/control-surface.ts, and AdapterManager in packages/server/src/adapter-manager.ts). The single-adapter OPENMIXER_ADAPTER env path still exists alongside it.