Skip to content

Nest

Organize hosts as a DOM-tree, classify them with Traits, apply Dendritic configs via CSS rules.

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.env = "prod";
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
nest.rules = [
{ is = nest.host; # defaults for all hosts
# Each rule is Dendritic: several configuration domains over same configuration aspect
darwin = ...;
nixos.nix.settings.experimental-features = [ "nix-command" "flakes" ]; }
{ is = nest.nginx;
nixos.services.nginx.enable = true; }
{ is = nest.lb;
nixos = { select, ... }:
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 # trait match
[ 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);
};
# Then use it in a rule
{ is = [ nest.host (nest.attrs { userCount = 0; }) ];
nixos.warnings = [ "host has no users" ]; }
Get Started How It Works
Contribute Community Sponsor