Decision records
ADR-0017: Admin API and operations
- Status: Accepted (2026-10-05)
Context
Handoff §11/M9: the admin API uses a Unix socket by default; remote administration only
when explicitly enabled and strongly authenticated (mTLS). §9: scalwsctl commands for
status, tenants, apps, top, reload, rollback, config-diff, diagnose, runtime
restart/scale. M9: completion, dashboard, systemd packaging, DEB/RPM, backup/restore of
configuration. Until now, mutating operations (cache purge, explain requests) lived on the
unauthenticated loopback metrics listener.
Decision
- Transport: HTTP/1.1 over a Unix socket,
server.admin.socket(default<runtime_dir>/admin.sock), created with mode0660(owner and group of the server process). Access control is the file system: who can open the socket is an operator. Every mutating request is written to the audit log (scalws::audit) with the peer’s uid/gid/pid (SO_PEERCRED). A live socket of another instance is never replaced; a stale one is. Remote administration (TCP + mTLS) is not implemented. - Split of surfaces:
- admin socket — everything, including mutations:
POST /v1/reload,POST /v1/rollback,POST /v1/apps/{tenant}/{app}/restart,POST /v1/apps/{tenant}/{app}/scale?workers=N,POST /v1/cache/purge,POST /v1/explain; and views:/v1/status,/v1/apps,/v1/tenants,/v1/config/history,/v1/config/diff,/v1/diagnose,/v1/optimizer,/v1/explain. - loopback metrics listener — read-only:
/metrics,/healthz,/status,/diagnose,/optimizer,GET /explain, and a read-only/dashboardpage. Cache purge and explain requests moved to the socket.
- admin socket — everything, including mutations:
- Reload and rollback: one controller performs every configuration change (SIGHUP,
API reload, rollback, restart): validate → prepare → atomic swap, serialised by a lock.
The last 10 applied configurations are kept in memory with generation, time and source;
rollback re-applies one of them through the same path (and is itself recorded).
config/diffis a line diff of the canonical JSON form of two kept generations. - Restart of an application prepares a fresh runtime instance for it (others are
reused), starts it and waits for readiness, swaps, then drains the old instance — no
dropped requests. Scale sets the worker target of a scalable pool within its bounds
(the optimizer may later move it, unless
autoscaleisoff/recommend). - Privileges: the packaged unit runs as user
scalwswithCAP_NET_BIND_SERVICEonly; scalws does not drop privileges in-process (a multi-threadedsetuidis avoided) and logs a warning when started as root. - Packaging: DEB and RPM built with nFPM from
packaging/nfpm.yaml: binaries, systemd unit,/etc/scalws/scalws.yaml(config, not replaced on upgrade),scalwssystem user, state directories.scalwsctl completion <shell>prints shell completions. - Backup/restore:
scalwsctl backupcopies the configuration and every file it references (certificates, keys, document roots excluded) with a manifest;scalwsctl restoreshows what it would write and only writes with--apply, keeping the replaced files as*.bak.
Consequences
- Read-only views stay reachable for local tools without socket permissions.
- The history is lost on restart; the configuration file and backups are the durable record.