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 mode 0660 (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 /dashboard page. Cache purge and explain requests moved to the socket.
  • 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/diff is 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 autoscale is off/recommend).
  • Privileges: the packaged unit runs as user scalws with CAP_NET_BIND_SERVICE only; scalws does not drop privileges in-process (a multi-threaded setuid is 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), scalws system user, state directories. scalwsctl completion <shell> prints shell completions.
  • Backup/restore: scalwsctl backup copies the configuration and every file it references (certificates, keys, document roots excluded) with a manifest; scalwsctl restore shows 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.