Traits
What a trait is
Section titled “What a trait is”A trait is a named tag that classifies a node. Traits are defined under nest.trait.<name> and referenced via the nest.<name> proxy module arg:
nest.trait.web = { }; # marker trait — classification onlynest.trait.nginx = { }; # marker traitnest.trait.host.class.nixos = …; # entity trait — produces outputNodes declare which traits they participate in via is:
nest.prod.web-1 = { is = [ nest.host nest.web ]; # this node is a host AND a web server};Entity traits
Section titled “Entity traits”A trait that defines a Nix configuration class is an entity trait. The class determines what kind of output this entity produces and how.
The class is an attrset of functions. Each key is an output class name, and the value is a function select: cfg: <output>:
# host entity — produces a nixosSystemnest.trait.host.class.nixos = select: modules: nixpkgs.lib.nixosSystem { system = select.node.system; inherit modules; };
# user entity — contributes to parentnest.trait.user.class = { nixos = select: modules: { nixos = modules; }; # unchanged contributions to nixos user = select: modules: { # forward class `user` to `nixos` nixos.users.users.${select.node.name} = lib.mkMerge modules; };};The class function receives:
select— the select context for this nodemodules— the list of configuration from all matching rules + child contributions
Marker traits
Section titled “Marker traits”A trait without a class is a marker trait — it classifies nodes but produces no output:
nest.trait.admin = { };nest.trait.deploy = { };nest.trait.nginx = { };Markers are useful for:
- Classification targets (
is = nest.admin) - Selector predicates (
is = [nest.host (nest.has nest.admin)]) - User registry entries with no independent output
needs — explicit dependencies
Section titled “needs — explicit dependencies”needs declares that having one trait implies also having others. Nest expands this dependency DAG automatically — you never wire traits together per node:
nest.trait.nginx = { };nest.trait.ssh = { };nest.trait.firewall = { };
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];nest.trait.web.needs = [ nest.server ];
# A node with is = [nest.host nest.web] automatically has:# web → server → nginx, ssh, firewall# Rules for nginx, ssh, firewall all fire on itneededBy — reverse injection
Section titled “neededBy — reverse injection”neededBy is the inverse of needs. Instead of a node opting into a trait, the trait declares what it attaches to:
nest.trait.monitoring.neededBy = nest.server;# Every node matching nest.server automatically gains nest.monitoring# No server definition needs to declare itneededBy accepts any selector — trait, compound, or string. It is re-evaluated iteratively until fixpoint, so chains of neededBy work:
nest.trait.node-exporter.neededBy = nest.monitoring;# monitoring → node-exporter → any server automatically gets bothsynth — Node’s attibute and children synthesis
Section titled “synth — Node’s attibute and children synthesis”synth runs after trait expansion, before rules fire. It computes derived attributes and can inject virtual children.
Derived attributes
Section titled “Derived attributes”nest.trait.host.synth = select: { node.userCount = builtins.length (select.children nest.user);};
# The userCount attr is now on every host node# Use it in a rule:{ is = [ nest.host (nest.attrs { userCount = 0; }) ]; nixos.warnings = [ "no users on this host" ]; }Virtual children
Section titled “Virtual children”nest.trait.host.synth = select: let adminUsers = select nest.admin; # nodes with marker trait in { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; # become real entity nodes }) adminUsers; };
# Every host now has alice and bob as DOM children# User rules fire on them, contributing nixos.users.users.* to the parent hostThe synth return format:
synth = select: { node = { derivedAttr = "value"; # merged onto the node children = [ # injected as DOM children { name = "alice"; is = [ nest.user ]; sshKeys = [ … ]; } ]; };};- Plain attrs (everything except
children) are merged onto the node - Children get
__pathand__parentPathset automatically - Children’s traits are fully expanded before rules run
Rule synth
Section titled “Rule synth”Rules can also carry a synth key — it works identically to trait synth but fires during rule evaluation. This lets the virtual children depend on the rule’s selector:
# Only prod hosts get admin users{ is = [ nest.host (nest.attrs { env = "prod"; }) ]; synth = { select, ... }: { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; }) (select nest.admin); };}