Architecture
Decision log
Significant decisions get an ADR in docs/adr/. Smaller ones are logged here with date
and reason so they can be revisited.
| Date | Decision | Reason |
|---|---|---|
| 2026-10-05 | ADR-0001 Rust + Tokio + hyper data plane | Handoff §1/§3 |
| 2026-10-05 | ADR-0002 rustls + aws-lc-rs | Mature TLS, PQ hybrid KX default, FIPS path |
| 2026-10-05 | ADR-0003 YAML via serde-saphyr, validate-then-swap | serde_yaml is deprecated upstream; atomic reload |
| 2026-10-05 | ADR-0004 Handler / RuntimeAdapter split | Keep language specifics out of the core |
| 2026-10-05 | ADR-0005 prometheus-client + tracing JSON, bounded labels | Handoff §10 |
| 2026-10-05 | ADR-0006 static confinement (canonicalise now, openat2 next) | Handoff §11 |
| 2026-10-05 | ADR-0007 Linux-first, cross-platform development | Dev machine is Windows + Docker |
| 2026-10-05 | Route matching uses the percent-decoded, dot-segment-resolved path | Prevents /%61dmin and /x/../admin route bypass |
| 2026-10-05 | Encoded slash (%2F) is preserved for routing and refused by static files | Avoids creating segment boundaries from encoded data; apps that need %2F still get it via proxy |
| 2026-10-05 | Requests with both Content-Length and Transfer-Encoding rely on hyper: TE wins, connection closes after the response | RFC 9112 §6.1; verified by cl_te_smuggling_does_not_reach_upstream |
| 2026-10-05 | Client-supplied Forwarded/X-Forwarded-*/X-Real-IP are always replaced | No trusted-proxy configuration exists yet; spoofing-safe default |
| 2026-10-05 | Connection limit pauses accept() instead of accept-and-close | Kernel backlog provides natural backpressure; no user-space queue |
| 2026-10-05 | Listener/metrics/max_connections changes require restart | Rebinding sockets during reload is out of M1 scope; explicit error instead of partial apply |
| 2026-10-05 | No tower middleware stack | Single explicit pipeline function is easier to audit; revisit when middleware count grows |
| 2026-10-05 | Benchmark load generator: oha 1.16.0, pinned CPUs per role | Reproducible, JSON output; server/upstream/load never share CPUs |
| 2026-10-05 | Static hot path does syscalls on the async worker only when the kernel guarantees they will not block (RESOLVE_CACHED, RWF_NOWAIT) | Removes the blocking-pool round trip that cost ~5 context switches per request |
| 2026-10-05 | Large-file chunk size 256 KiB (64 KiB measured 30% slower) | scripts/profile.sh static-1m |
| 2026-10-05 | Benchmarks and profiles serve the document root from a Docker volume (ext4), not overlayfs | overlayfs defeats RESOLVE_CACHED; production roots are on real filesystems |
| 2026-10-05 | ADR-0008 managed processes: no shell, cleared env, process groups, 5 crashes/min → Failed | Handoff §13 |
| 2026-10-05 | ADR-0009 PHP via fastcgi-client (small crate, wrapped, reviewed) + httparse for CGI headers | Handoff §1 “use mature libraries”; replacement path documented |
| 2026-10-05 | WSGI is served by gunicorn only | uvicorn’s WSGI interface is deprecated; hypercorn’s is less used |
| 2026-10-05 | Python: one supervised server process per instance, its own --workers | The application servers already manage worker processes well |
| 2026-10-05 | Node: N supervised processes, one socket each, round-robin over ready ones | Node has no built-in multi-process server; keeps crashes per worker |
| 2026-10-05 | PHP-FPM workers run as nobody when scalws runs as root | PHP-FPM refuses root workers; per-tenant users arrive in M3 |
| 2026-10-05 | WebSocket tunnels are limited server-wide, not per connection slot | Upgraded IO leaves hyper’s connection accounting; a separate semaphore bounds it |
| 2026-10-05 | Persistent FastCGI connections only for managed pools; replay only buffered bodies (≤ 64 KiB) | Kept-alive connections pin FPM workers; a stale connection must never lose a non-replayable request |
| 2026-10-05 | No LSAPI adapter for performance | Profiling showed the gains attributed to LSAPI (persistent connections, less setup) are available over FastCGI; PHP execution is the remaining cost |
| 2026-10-05 | Benchmark container needs --cap-add SYS_NICE | Re-pinning PHP-FPM workers (another uid) to upstream CPUs requires it; the harness warns otherwise |
| 2026-10-05 | ADR-0010 cgroup placement via a fixed /bin/sh trampoline | std/tokio offer no clone3(CLONE_INTO_CGROUP) and pre_exec needs unsafe; moving after spawn would let early forks escape |
| 2026-10-05 | memory.swap.max defaults to 0 when a memory limit is set | Measured: without it a 200 MiB allocation under a 50 MiB limit succeeded via swap |
| 2026-10-05 | io.weight best effort | Not available with the WSL2 kernel’s I/O scheduler; skipped with one warning |
| 2026-10-05 | Tenant request quotas instead of attributing scalws CPU to tenants | Handoff §7: do not claim cgroups isolate CPU used inside the shared server |
| 2026-10-05 | Per-tenant Unix users designed, not implemented | Handoff §7 lists them as a later hardening feature |
| 2026-10-05 | ADR-0011 profiles as embedded YAML data + small fact extractors in code | Handoff §6 “data-driven”; facts like a Python app object cannot be expressed as data |
| 2026-10-05 | profile: never inspects application files at load time | A deploy must not silently change how an application runs (handoff §6) |
| 2026-10-05 | Explicit runtime overrides a profile’s runtime as a whole | Predictable; field-level merging deferred |
| 2026-10-05 | ADR-0012 cache: moka storage, explicit freshness only, opt-in per app | Mature eviction; never infer cacheability (handoff §8) |
| 2026-10-05 | ADR-0013 per-source state: 64 shards, fixed capacity, evict idle, fail open when all active | Memory bounded by configuration, not by attacker address count |
| 2026-10-05 | Deny rules answer 404, not 403 | Do not confirm that a sensitive resource exists |
| 2026-10-05 | Purge endpoint on the loopback metrics listener | Interim admin surface until the authenticated admin API (M9) |
| 2026-10-05 | Property-based fuzzing (proptest) instead of cargo-fuzz for now | Runs on stable in every test run; coverage-guided fuzzing needs nightly |
| 2026-10-05 | ADR-0014 optimizer: pure engine, caller supplies time and applies decisions | Deterministic, unit- and property-testable without clocks |
| 2026-10-05 | First optimizer action: Node worker targets only | Only runtime where scalws owns individual workers; others get recommendations/advisories |
| 2026-10-05 | workers.autoscale default recommend | Handoff §8: recommend-only by default |
| 2026-10-05 | Disposition names applied/rolled_back identical in JSON, metrics and logs | One vocabulary for operators and tooling |
| 2026-10-05 | ADR-0015 diagnostics: pure scalws-diag, evaluated in the optimizer’s window loop | One sampling path, deterministic and testable; works with the optimizer disabled |
| 2026-10-05 | eBPF deferred | Handoff: optional enrichment only; every M7 finding comes from cgroup files and counters |
| 2026-10-05 | Phases: time to headers and transfer only | scalws rejects instead of queueing, so there is no queue phase; connect time hidden in pooled clients |
| 2026-10-05 | ADR-0016 AI advisor: OpenAI-compatible HTTP provider, disabled by default | Works with llama.cpp/Ollama/vLLM/LM Studio without hard-coding a model |
| 2026-10-05 | Remote model endpoints need allow_remote: true; HTTPS deferred | Telemetry stays on the host unless the operator decides otherwise |
| 2026-10-05 | Validate model output even with response_format | Not every server enforces schemas; nothing unvalidated is shown |
| 2026-10-05 | ADR-0017 admin API on a Unix socket in scalws-server, access by file permissions + audit | Handoff §11; no network exposure; peer credentials attributable |
| 2026-10-05 | No in-process privilege drop | Unit runs unprivileged with CAP_NET_BIND_SERVICE; multi-threaded setuid avoided |
| 2026-10-05 | Rollback re-applies a kept configuration as a new generation | One code path for every change; history stays linear and auditable |
| 2026-10-05 | Packages with nFPM, stripped binaries | One definition for DEB and RPM; 9.6 MB instead of 30 MB |
| 2026-10-05 | Gate 6 verified with pass/fail scripts in the bench image | Repeatable on any host; criteria in bench/README.md |
| 2026-10-05 | ADR-0018 ACME with instant-acme, HTTP-01 only, renewal via controller re-apply | Pure Rust/rustls/aws-lc-rs; one activation path; no restart |
| 2026-10-05 | Allow licence CDLA-Permissive-2.0 | Mozilla root-certificate data pulled in by the ACME client’s HTTPS stack |
| 2026-10-06 | ADR-0019 HTTP/3 opt-in feature, same pipeline via call_boxed | h3 is 0.0.x; one request path for all protocols |
| 2026-10-06 | ADR-0020 containers via a daemonless engine CLI as supervised workers | Reuses supervision, pools and tenancy; no daemon or OCI tooling of our own |
| 2026-10-06 | --init and a post-exit rm --force for container workers | PID-1 servers ignore SIGTERM; killed podman run leaves containers behind |
| 2026-10-06 | ADR-0021 multi-node: pull-based controller, signed bundles, existing apply path on nodes | No distributed store; control-plane outage blocks changes, never serving |
| 2026-10-06 | Proxy drives upstream connections in the request task (own pool) | Profiling: the task hop of hyper-util’s client dominated; +28 % rps |
| 2026-10-06 | Per-core runtimes not adopted | Measured upper bound ≈ +10 % for proxying; not worth the complexity now |
| 2026-10-06 | ADR-0022 trusted proxies: right-most untrusted XFF entry; PROXY v1/v2 from trusted peers only | Spoof-resistant client address for limits, logs and applications |
| 2026-10-06 | ADR-0023 remote admin: mTLS only, same router as the socket | Handoff §11: remote only explicitly and strongly authenticated |
| 2026-10-06 | ADR-0024 cache disk tier: one file per variant, writer thread, index rebuilt at start | No new dependency; request path never blocks on disk |
| 2026-10-06 | ADR-0025 eBPF: cgroup_skb network accounting only, C program + Aya, feature-gated | The one signal cgroup v2 lacks; no nightly toolchain; never required |
| 2026-10-06 | Allow licence Zlib | foldhash via aya (optional eBPF feature); OSI-approved permissive |
| 2026-10-06 | ADR-0026 per-tenant Unix users via setpriv; CAP_KILL required | Kernel drops the parent-death signal and stop signals to other uids without it (found in testing) |
| 2026-10-06 | ADR-0027 sendfile via placeholder buffers recognised by address in the I/O layer | No unsafe, no hyper fork; byte-exact tests guard the hyper queue-strategy dependency |
| 2026-10-06 | ADR-0028 open file cache: identity check per hit (statx), optional validity window | +10 % small files; skipping the check gained nothing more |
| 2026-10-06 | ADR-0029 per-core event loops sharing one listening socket | +38 % upper bound on respond; shared socket balances long-lived connections better than SO_REUSEPORT hashing |
| 2026-10-06 | Keep hyper; drop hyper-util auto-detection | Bare hyper per core ≈ nginx; auto layer cost 14 %; own parser only +21 % at high risk |
| 2026-10-06 | mimalloc default | Proxy +17 %, cache hit +26 % (2-run average), +20 MiB RSS |
| 2026-10-06 | ADR-0030 kTLS opt-in | HTTPS 1 MiB 3.9k → 6.2k req/s; needs the tls module |
| 2026-10-06 | ADR-0031 .htaccess with the linear-time regex crate | Tenant-controlled input: no backtracking, no proxy, bounded |
| 2026-10-08 | ADR-0042 per-domain counters by configured pattern, indexed and sharded per event loop | Cardinality set by configuration, not clients; no request-path hashing; gate: no regression |