ADR 005 — Monobinary Web Server
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/decisions/005-monobinary-web-server.md. Edit at the source, not here.
ADR 005 — Monobinary Web Server
Section titled “ADR 005 — Monobinary Web Server”Status: Accepted Date: 2026-04-18 Deciders: SYNNET Engineering
Context
Section titled “Context”SynVirt needs to serve a management web interface to operators. The web interface must be:
- Available over HTTPS (port 443 in production, 8443 in dev)
- TLS-terminated at the management plane — no unencrypted traffic in any mode
- Served from the control plane node without requiring additional software installation
- Simple to deploy: installing SynVirt should not require configuring a separate web server process
- Maintainable: adding API endpoints should not require touching a reverse proxy configuration file
Decision
Section titled “Decision”Embed the Axum HTTP server directly in synvirt-daemon. The daemon serves
both the REST API and the static frontend assets from a single binary and a
single process. Static assets (HTML, CSS, JS) are served from
/opt/synvirt/web/ in production and ./web/ in dev mode.
TLS is handled natively by Rustls within the daemon. No external TLS termination (no nginx, no HAProxy, no stunnel).
Rationale
Section titled “Rationale”- Zero external dependencies for the web server: The daemon is a Rust
binary with no external web server process. Installing SynVirt is
systemctl enable --now synvirt-daemon. Uninstalling it issystemctl disable synvirt- daemon && rm /opt/synvirt/bin/synvirt-daemon. There is nothing else to manage. - Single process to manage: On a hypervisor node, every running process is a potential source of resource contention and failure. Combining the API server and web server into one process means one PID, one systemd unit, one set of logs, one restart policy. Operators do not need to distinguish between “the API is down” and “the web server is down”.
- TLS handled natively via Rustls: Rustls is compiled into the binary. The daemon owns its TLS configuration, certificate paths, and renewal logic. There is no nginx.conf or TLS passthrough configuration to maintain. In Hito 0/0.3, the cert is self-signed and auto-generated. In later hitos, cert management (Let’s Encrypt ACME or customer-supplied certs) will be handled by the daemon itself.
- Axum static file serving is first-class: Axum’s
tower-httpintegration providesServeDirmiddleware that efficiently serves static files with correct MIME types, cache headers, and range request support. This is not a compromise; static file serving from Axum is production-quality. - Simplified development: During development,
cargo run -- --devstarts everything needed to work on both the API and the frontend. There is no “run nginx separately” step.
Consequences
Section titled “Consequences”- Static assets must be deployed alongside the binary: The binary does not
embed the frontend assets (they are not
include_bytes!-ed into the binary). The deployment package must include both the binary at/opt/synvirt/bin/synvirt-daemonand the built frontend at/opt/synvirt/web/. The ISO builder and any future package manager (.deb) must include both. - Frontend build output is decoupled from the Rust build: The frontend (Next.js from Hito 1 onward) must be built separately and its output placed in the correct location before the daemon can serve it. The dev workflow requires running the frontend build before or alongside the daemon.
- No dynamic virtual host routing: With no reverse proxy, we cannot serve multiple virtual hosts from the same IP/port without implementing vhost logic inside the daemon. This is not a current requirement; all traffic goes to the SynVirt management interface.
- Certificate management lives in the daemon: In Hito 0/0.3 this is simple (self-signed). In later hitos, automatic cert renewal (ACME/Let’s Encrypt) must be implemented in Rust within the daemon, rather than delegated to certbot running against nginx. This is more work but results in a more cohesive system.
Alternatives Considered
Section titled “Alternatives Considered”nginx as a reverse proxy
Section titled “nginx as a reverse proxy”- Pros: Battle-tested, widely known, handles TLS termination well, easy to configure static file serving.
- Cons: Adds a second process and a second configuration file to every SynVirt node. Certificate management must be split between nginx (TLS) and the daemon (API). Operators must manage nginx restarts on cert renewal. Adds ~5 MB of additional packages to the minimal Debian install. Two processes means two failure modes to explain to customers.
- Decision: Rejected. Complexity cost outweighs the familiarity benefit.
Separate web service (a standalone file server)
Section titled “Separate web service (a standalone file server)”- Pros: Clean separation of concerns between API and static files.
- Cons: Two processes with independent restart policies, log streams, and TLS configurations. Any boundary between the processes requires authentication/authorization to be applied twice (once at the web server, once at the API). More deployment artifacts. More systemd units.
- Decision: Rejected. Separation of concerns at the process level is not worth the operational complexity for a management interface that serves a single purpose.