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.

HandlerResponsibilityMutates
observe()Report current live stateno
plan()Diff desired against observed, emit actionsno
apply()Execute approved actionsyes
verify()Assert live postconditions holdno

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

ArtifactHoldsNever holds
Consumer Nix modulesDesired state, secret referencesSecret values
Canonical JSONThe evaluated specificationSecret values
Persisted planOrdered actions, risk, digestSecret values
Approval evidenceDigest, principal, risk, expirySecret values
JournalPlan identity, redacted transitionsSecret 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: