Architecture
NXD keeps three concerns apart. The consumer repository owns what the infrastructure should be, the core engine owns what must change and in what order, and providers own how a specific system is actually changed.
Each layer may only consume what the previous one produced. That boundary is what makes the engine testable and the providers replaceable.
01 Consumer repository
Topology, host inventory, addresses, storage, access rules, and SOPS/age encrypted secrets — as Nix modules. Secrets appear as references, never values.
02 Core engine nxd-core
Canonical JSON parsing and validation, action-graph scheduling, risk classification, plan persistence, approval evidence, and the durable journal.
03 Typed Rust providers
Nix, Proxmox VE, Proxmox Backup, Tailscale/Headscale, Identity, VMware, DigitalOcean, and WSL — each owning its own protocol and nothing else.
The core never speaks a provider’s protocol, and a provider never decides ordering. A provider that scheduled its own work would break the one property the design exists to provide: that ordering across provider boundaries is decided in a single place.
Evaluation is offline
nxd.lib.evalConfiguration turns consumer modules into a canonical
JSON specification. It contacts no endpoint and decrypts no secret.
This is a correctness property, not a performance one. If evaluation could read live state, a plan would depend on the thing it is planning against, and two runs against a drifting endpoint could legitimately disagree.
The provider port
Every provider implements four handlers. Only one of them mutates.
| Handler | Responsibility | Mutates |
|---|---|---|
observe() | Report current live state | no |
plan() | Diff desired against observed, emit actions | no |
apply() | Execute approved actions | yes |
verify() | Assert live postconditions hold | no |
An action emitted by plan() is a proposal, not a commitment.
Nothing runs until a plan containing it has been persisted, hashed, and
approved against that exact digest.
Scheduling
The scheduler starts an action as soon as its complete dependency set has
succeeded and no conflicting resource is locked. There are no artificial batch
waves, so independent work on unrelated hosts proceeds concurrently — bounded
by --parallel, and guarded by ownership locks so two actions never
mutate the same resource at once.
If a prerequisite fails, its dependants remain unexecuted rather than being reported as provider failures.
One shared transport
Every provider reaches remote machines through nxd-transport,
which enforces exact Ed25519 host key pinning, reuses SSH connections by
default (ControlMaster), routes through declared jump hosts, and
bounds every subprocess with timeouts and output sanitization.
Verification and connection reuse are one decision enforced in one place. Before the transport was shared, twelve non-test modules across four providers invoked SSH and exactly one of them multiplexed.
Where state lives
| Artifact | Holds | Never holds |
|---|---|---|
| Consumer Nix modules | Desired state, secret references | Secret values |
| Canonical JSON | The evaluated specification | Secret values |
| Persisted plan | Ordered actions, risk, digest | Secret values |
| Approval evidence | Digest, principal, risk, expiry | Secret values |
| Journal | Plan identity, redacted transitions | Secret values |
The Nix store is world-readable, so nothing sensitive is placed in it. Secrets
are resolved at apply time by SOPS/age into process memory, or into
0600 staging files when a subprocess needs a path.
Read further
The technical manual covers each of these in depth:
- Architecture overview — ownership boundaries and the provider port
- Safety model — the refusal set and approval binding
- Transport and connections — host key pinning, multiplexing, jump hosts
- Performance model — selected-first evaluation and concurrency
- Installation and recovery — install modes, bootstrap trust, artifacts
- Output and logs — progress, log files, profiling
- Choose where a system is built — build placement and low-memory targets