Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

NXD is a declarative infrastructure reconciliation engine for Nix-authored desired state. It evaluates consumer Nix modules into canonical JSON, plans structural and host changes as a dependency-ordered graph, applies persisted plans under digest approval, and verifies the result through typed Rust providers.

This manual documents behavior that exists in the tree. NXD is pre-release: where a capability is planned rather than shipped, the page says so.

Where to start

If you want toRead
Run NXD against disposable fixtures firstQuick start
Check a configuration without touching infrastructureValidate configuration
Understand why the engine is shaped this wayArchitecture overview
Know what NXD refuses to do, and whySafety model
Look up a command, flag, or resource fieldCLI reference, Resource schema
Diagnose a failureCommon failures

The pipeline

Every NXD invocation moves through the same stages. The boundaries between them are the load-bearing part of the design: each stage may only consume what the previous one produced.

Nix flake modules
      │
      ▼  evalConfiguration — offline, no network, no decryption
Canonical JSON specification
      │
      ▼  nxd plan — dependency-ordered action graph + risk classification
Persisted plan  (sha256:…)
      │
      ▼  approval of that exact digest
Provider mutation
      │
      ▼  nxd verify — live state re-read
Post-apply verification and idempotency check

Key concepts

Nix-authored canonical configuration

Site topology, host inventory, hypervisor settings, and service attachments are authored in Nix modules under nxd.site, nxd.providers, nxd.secrets, and nxd.operations. nxd.lib.evalConfiguration exports that desired state as canonical JSON.

Evaluation is strictly offline. It contacts no endpoint and decrypts no secret, so planning cannot be influenced by the state of the infrastructure being planned against.

Canonical JSON specification

One reproducible document describing the full desired state:

  • deploymentTarget — host declarations, build settings, SSH identities, and plane attachments.
  • providerInstances — provider configuration (Nix, PVE, PBS, VMware, Headscale, Identity, DigitalOcean, WSL).
  • secretBindings — references to SOPS/age documents. References only; never values.
  • resources — provider-owned managed resources such as pveVm, pbsDatastore, and headscaleNode.

Dependency-ordered action graph

The scheduler computes an acyclic graph ordering actions across every provider, rather than running per-provider steps in sequence.

Prerequisites are edges, not conventions. Minting a Headscale preauth key or resolving an SSH host key is ordered before the target activation that consumes it, because the dependency is recorded in the graph.

Every action carries a risk classification, ordered ReadOnly < Reversible < ServiceImpacting < Destructive < IdentityCritical. The classification is visible at approval time, so the class of change is known before it is authorized, and evidence records the highest risk in the plan. See the safety model for what each level means.

Digest-bound approval

A plan is persisted to disk and hashed. Applying requires approval of that exact digest. If configuration changes between planning and applying, the digest no longer matches and the apply refuses rather than executing a plan nobody reviewed.

See the safety model for the full refusal set.

Secret mediation

Secrets stay out of Nix store paths and out of canonical JSON. Configuration carries binding references such as secret/bar/hosts/medo/ssh-host-ed25519-private-key, and nothing else.

At apply time, SOPS/age resolves those references directly into process memory, or into staging files with 0600 permissions when a subprocess requires a path. The Nix store is world-readable, which is precisely why nothing sensitive is placed in it.

Idempotent verification

Success is a re-read of live state, not a zero exit code. Every lifecycle phase verifies the target against the desired closure, and a converged system replans to zero actions with desiredSystemPath == activeSystemPath and systemPathMatches: true.