Skip to content

Traits

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 only
nest.trait.nginx = { }; # marker trait
nest.trait.host.class.nixos = …; # entity trait — produces output

Nodes 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
};

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 nixosSystem
nest.trait.host.class.nixos =
select: modules:
nixpkgs.lib.nixosSystem {
system = select.node.system;
inherit modules;
};
# user entity — contributes to parent
nest.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 node
  • modules — the list of configuration from all matching rules + child contributions

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 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 it

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 it

neededBy 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 both

synth — 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.

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" ]; }
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 host

The 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 __path and __parentPath set automatically
  • Children’s traits are fully expanded before rules run

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);
};
}
Contribute Community Sponsor