The DOM
Nodes and namespaces
Section titled “Nodes and namespaces”Nest’s DOM walk distinguishes between two kinds of attrset:
Namespace — an attrset without is. Acts as a container that passes scalar attributes down to child nodes. The namespace itself is not a node.
Node — an attrset with is = [ … ]. Becomes a DOM node with a path, parent, and traits.
nest.prod.system = "x86_64-linux"; # namespace attr — inherited by childrennest.prod.env = "prod"; # namespace attr — inherited by children
nest.prod.web = { # NODE — has is = [...] is = [ nest.host nest.web ]; addr = "10.0.0.2";};The prod attrset has no is, so it is a namespace. Every scalar attribute in prod (system, env) flows down to web automatically.
Attribute priority
Section titled “Attribute priority”When multiple sources define the same attribute, they merge in this order (lowest to highest):
- Inherited namespace attrs — propagated from ancestor namespaces
- Node own attrs — defined directly on the node
- Synthesized attrs — values derived from querying other nodes
A node’s own attribute always wins over what was inherited:
nest.prod.env = "prod";
nest.prod.web = { is = [ nest.host ]; env = "override"; # wins over inherited "prod"};# web.env == "override"Paths and Marker traits
Section titled “Paths and Marker traits”To scope siblings, nest your environments under a parent node labeled with a marker trait.
nest.prod.router = { is = [ nest.cluster ]; };nest.prod.router.web-1 = { is = [ nest.host ]; }; # __parentPath = "prod.router"nest.prod.router.web-2 = { is = [ nest.host ]; }; # __parentPath = "prod.router"# Now web-1 and web-2 are true siblings, scoped to routerNode context
Section titled “Node context”Every node carries a context object used by selectors and rule functions. The context provides:
ctx.children # [ node ] — direct DOM children (parentPath == node.__path)ctx.ancestors # [ node ] — all ancestors up to root, nearest firstctx.allNodes # [ node ] — every node in the DOMctx.select # select API (see Select API reference)Contexts are built from the fully synthesized node list — after trait expansion, neededBy injection, and synth runs. Selectors and rule functions always see the complete picture.
Marker-only nodes
Section titled “Marker-only nodes”A node with only marker traits (no class) is still a valid DOM node. It has no output (processNode returns null), but it is fully visible to select:
nest.users.alice = { is = [ nest.admin ]; # admin has no class — alice produces no nixosConfiguration sshKeys = [ "ssh-ed25519 …" ];};
# In a rule:nest.rules = [{ is = nest.host; synth = { select, ... }: { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user ]; }) (select nest.admin); # finds alice };}];This is how user registries work in Nest — define users as marker nodes, synthesize them as typed children on each host.
Virtual children via synth
Section titled “Virtual children via synth”Trait’s synth and Rule’s synth can inject virtual children into a node. These children are indistinguishable from static DOM children once injected:
- They get
__path = "${parent.__path}.${child.name}" - Their traits are fully expanded (
needs+neededBy) - Rules are matched and applied to them
collectChildFragspicks their class contributions up into the parent
See Traits for the full synth API.