DOM Topology
Declare hosts, users, and any entity as an attrset hierarchy. Parents pass attributes down to children automatically.
There no mandated topology, you are free to structure your infra as you like
and use CSS selectors to apply configuration to nodes.
Trait Classification
Classify nodes with traits. Traits form dependency DAGs via needs/neededBy — declare server and get nginx, ssh, firewall for free.
Traits also allow data sharing between
Nodes subscribing to same trait.
CSS-style Rules
Rules match nodes by selector and contribute Dendritic Nix configs.
Write selectors as strings or via typed combinators.
is = ".host:has(.admin)" is the same as
is = [host (has admin)], a host with at least one admin user.
Cross-Node Dependencies
Rule configurations can use the select query helper to retrieve
siblings or other nodes from the DOM allowing dependent configuration.
select.siblings web returns all web peers, build haproxy backends, /etc/hosts, or user lists dynamically.
These three concepts build the configuration pipeline:
# 1. Traits with `class` define configurable entity types
nest.trait.host.class.nixos = select: modules:
nixpkgs.lib.nixosSystem { system = select.node.system; inherit modules; };
# Traits define dependencies between them forming a DAG
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];
nest.trait.web.needs = [ nest.server ];
nest.trait.monitoring.neededBy = nest.server; # auto-inject on all servers
# 2. DOM: model your topology as an attrset hierarchy that makes sense to you.
# Anything outside nest.{trait,rules} is part of the DOM tree.
nest.prod.system = "x86_64-linux";
nest.prod.lb = { is = [ nest.host nest.lb ]; addr = "10.0.0.1"; };
nest.prod.web = { is = [ nest.host nest.web ]; addr = "10.0.0.2"; port = 80; };
nest.staging.system = "x86_64-linux";
nest.staging.env = "staging";
nest.staging.web = { is = [ nest.host nest.web ]; addr = "10.1.0.2"; port = 80; };
# 3. Rules: match selectors → contribute NixOS config
{ is = nest.host; # defaults for all hosts
# Each rule is Dendritic: several configuration domains over same configuration aspect
nixos.nix.settings.experimental-features = [ "nix-command" "flakes" ]; }
nixos.services.nginx.enable = true; }
let webs = select.siblings nest.web; # obtain from peer web-trait hosts
in { services.haproxy.backends = map (w: "${w.addr}:${toString w.port}") webs; }; }
Nest Supports string selectors or DSL combinators.
Match nodes by any combination of trait, name, attribute, or relationship:
"#web-1" # by name (CSS #id)
".lb" # by trait (node type)
"[env=prod]" # by attribute (CSS [attr=val])
"prod > web" # child combinator
[ nest.host nest.web ] # AND compound
nest.or [ nest.web nest.lb ] # OR
nest.not nest.staging # NOT
nest.has nest.admin # has child with trait
nest.within nest.prod # nested under ancestor
nest.attrs { env = "prod"; } # attribute equality
nest.when ({ host }: host.port > 1024) # predicate function
Declare server once and every trait it needs follows:
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];
neededBy works in reverse; monitoring.neededBy = nest.server injects monitoring into every server without touching any server definition.
synth allows adding derived attributes to nodes and injecting virtual children after querying the state of the tree:
# Add new userCount attribute on each host.
nest.trait.host.synth = select: {
node.userCount = builtins.length (select.children nest.user);
{ is = [ nest.host (nest.attrs { userCount = 0; }) ];
nixos.warnings = [ "host has no users" ]; }
Get Started
How It Works