No duplication
One topology generates all environment configs.
One fleet topology, multiple environments. Rules apply everywhere or only where you say.
graph TD
subgraph prod
LB["lb-prod\nlb + host"]
W1["web-prod-1\nweb + host"]
W2["web-prod-2\nweb + host"]
end
subgraph staging
WS["web-staging\nweb + host"]
end
Each sub-tree is a namespace. Attributes defined on the namespace flow to all nodes inside it:
nest.prod.system = "x86_64-linux";nest.prod.env = "prod";nest.prod.frontend.server.port = 8080;
nest.prod.frontend.lb.is = [ nest.lb ];nest.prod.frontend.server.web-1.is = [ nest.web ];nest.prod.frontend.server.web-2.is = [ nest.web ];All prod hosts inherit env = "prod".
All staging hosts inherit env = "staging".
Rules can match on these prod web[port=8080].
Service traits chain together so you only mark what matters:
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];nest.trait.lb.needs = [ nest.server ];nest.trait.web.needs = [ nest.server ];
nest.trait.monitoring.neededBy = [ nest.server ];graph LR
LBT["lb"] --> S["server"]
WT["web"] --> S
S --> N["nginx"]
S --> SH["ssh"]
S --> F["firewall"]
S -.->|neededBy| M["monitoring"]
monitoring.neededBy = nest.server means every server node automatically gets monitoring — including both lb and web nodes.
Rules receive a select argument that scopes queries to the current namespace:
{ is = nest.lb; nixos = { select, ... }: let webs = select.siblings nest.web; in { services.haproxy.config = mkHaproxy webs; };}
{ is = nest.host; nixos = { select, ... }: let peers = select.siblings nest.host; in { networking.extraHosts = mkHosts peers; };}The prod load balancer only sees prod web servers. /etc/hosts on each prod host only lists prod peers. Namespace boundaries are scoping boundaries.
Define data once in a subtree. Rules can fetch and inject them as children of the right nodes:
flowchart LR
A["nest.people.alice\nadmin"]
B["nest.people.bob\ndeveloper"]
subgraph prod["prod hosts"]
P["alice only"]
end
subgraph staging["staging hosts"]
S["alice + bob"]
end
A --> prod
A --> staging
B --> staging
# Prod: admins onlynest.rules."host[env=prod]" = { synth = { select, ... }: { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; }) (select "people:is(admin)"); };}Update a user’s SSH key once. It propagates to every host where rules place them.
See templates/fleet-demo for the complete working example.
No duplication
One topology generates all environment configs.
Add hosts freely
New host gets all matching rules automatically.
Role-based access
Users defined once, placed by rules per environment.
Dynamic config
LB discovers backends, /etc/hosts discovers peers. No hardcoding.