0003 — The server is the single source of truth; surfaces are thin clients

Status: Accepted (built) Date: 2026-06-29

Context

A live mix is driven from several places at once: a tablet at front-of-house, a phone on stage, a laptop, and — designed for S3 — two or three physical X-Touch fader banks. They must all show the same values. If two surfaces each held their own authoritative state and synced peer-to-peer, they would drift, fight, and need conflict resolution. That is the failure mode of every "everybody is a master" design.

Decision

The server holds the canonical state. Every surface — web UI, future MCU bank, another desk routed in — is a bidirectional client of that one server and nothing else. Any edit (a finger on glass, a motor fader, a device-side change) is sent to the server; the server applies it through the engine and broadcasts the resulting value to every connected client, including the originator. Surfaces never talk to each other.

In the code: MixerServer.broadcast() fans every EngineEvent to all sockets; the originating client also applies its change optimistically and reconciles against the echoed authoritative value. A connecting client is greeted with the current topology and can subscribe to have state (re)sent.

Consequences

  • All surfaces converge on one truth. There is no surface-to-surface protocol to design, and no distributed-consensus problem.
  • Adding a surface type is adding a client, not a new authority. The designed MCU/X-Touch layer is "one more client"; multi-window is "more clients of the same server".
  • Self-echo is deliberate: the server echoes to the originator too, so optimistic UIs have an authoritative value to reconcile to. (Some consoles suppress echo of a client's own writes — e.g. the X32 — so adapters apply optimistically and poll on connect; that is a device quirk, not openmixer's policy.)
  • The server is a single point of failure for control. It is stateless enough to restart and re-hydrate from device state + persisted sessions; clients reconnect and re-render.
  • Built today: greet-on-connect, subscribe, and full broadcast of state / meters / plugin.chain. The reconnect snapshot-on-connect for an arbitrary surface slice (so a reconnecting MCU bank repaints correctly) is part of the S3 design.

Note — 2026-07-29: the reconnect snapshot is built, and the X-Touch banks are real

Consequences says the reconnect snapshot-on-connect for an arbitrary surface slice, "so a reconnecting MCU bank repaints correctly", "is part of the S3 design". It is built and pervasive, though not the way this ADR describes: every ?watch=1 stream's first event is a full snapshot of that row (packages/server/src/resource-stream.ts), so a reconnecting client — including the selection row at /surface/selection (packages/server/src/surface-selection-row.ts) — is whole again the moment it re-opens, with no separate replay path. The physical X-Touch banks the Context anticipates are also real — packages/adapter-xtouch/.