For AI agents
Check a change against every system it touches before it lands. A type mistake fails with an error that names the option.
A package and the libraries it depends on form a graph. So does a machine’s configuration, and the network that machine sits on, and the services that call each other across it, and the clusters and cloud accounts those services are deployed to. Every layer of what you run has the same shape: things that need other things.
Each layer is described in its own language, by its own tool. You tie them together by hand, at the seams between tools, and the seams are where things go wrong.
You write the modules and wire them together. You remember which machine needs which user, which
user needs which shell, which service needs which port opened in which firewall, and you encode all
of it by hand, in the order each tool happens to need it. Each relationship lives in the shape of
the code: a feature reaches a machine because a file sits in its imports list, a value reaches a
module because three others pass it along, a setting applies because an mkIf somewhere tests the
right flag. Move an entry, rename an argument or reorder a list, and the relationship changes with
no error to say so. The relationships are the interesting part, and the evening goes on plumbing.
gen gathers every layer into one graph. Machines, users, services and features become nodes, and every relationship between them becomes an edge you can follow. You write a feature once, and every place that uses it receives its own tailored copy. Rules draw new connections from the ones you wrote. Then you ask the graph a question, and it answers: what runs where, what depends on what, and which file decided each value. One machine and a thousand machines are the same question, asked of a bigger graph.
gen is one evaluation plane for every system you run. One graph, evaluated once, hands each ecosystem its finished configuration, and that ecosystem’s own builder takes it from there: NixOS’s system builder, or a terranix or nixidy terminal you supply.
gen is built on Nix, which already describes software and machines as code and builds them the same way every time. gen gives that code a shape you can reason about.
Anyone who has to know what a change will touch before it ships.
For AI agents
Check a change against every system it touches before it lands. A type mistake fails with an error that names the option.
For operations engineers
See what each machine is declared to run, and why: which features it selected, which features those include, and which rule added a relationship.
For developers
See what gets built for each machine, and which file set each declared option, at which priority.
Four ideas carry the rest of gen. Each has its own page.
Relationships are edges
Machines, users and features are nodes. What relates them is a labelled edge you can query. Your Configuration as a Graph
Features are written once
A feature takes its machine as an argument, and gen passes the machine in. Reusable Features
Rules add relationships
A rule relates nodes when its conditions hold. gen solves all rules together. Rules That Add Relationships
Everything has an identity
Every node gets an identity computed from its content, which gen uses to reuse work. Stable Identity for Everything
A feature can carry configuration for more than one destination at once: a machine’s operating system, a user’s home directory, a cluster, a cloud account. gen calls each destination a class, and hands each class’s configuration to a terminal, the builder for that destination. The same graph feeds all of them, so a connection you declare once reaches every layer it touches.
The graph reaches down to the code you build on, too. Hand gen’s graph library the inputs recorded
in a flake’s flake.lock, and it tells you every input that depends on a given library. Pointed
at gen’s own lock file, it finds that 18 of gen’s 24 inputs are built on gen-prelude.
| Destination | What gen does there |
|---|---|
| Linux servers, with NixOS | Hands each machine’s configuration to NixOS’s system builder. gen’s flake-parts module provides this terminal. |
| Macs and home directories, with nix-darwin and home-manager | Hands darwin and home-manager configuration to a terminal you register for that class, built from gen-bind’s hosted terminal over the tool’s own builder. |
| Kubernetes, with nixidy | nixidy turns Nix into Kubernetes deployments for Argo CD. With a nixidy terminal you supply, gen hands it a service’s manifests, from the same graph that places the service on its machines. |
| Cloud infrastructure, with terranix | terranix writes Terraform in Nix. With a terranix terminal you supply, gen hands it firewall rules and cloud resources, drawn from the same edges that connect the services behind them. |
| Any other Nix module system | gen-bind passes graph values into any module written as a function of named arguments, and an outputs terminal hands configuration to any builder you supply. |
gen stands on decades of published work, and each part of it has a proof behind it. Finding a setting is name resolution over a scope graph, the same technique language tools use to resolve imports. Rules form a Datalog program with stratified negation, solved under the well-founded semantics, so a rule can depend on another being false and every answer stays well defined. Rebuilding after an edit is demand-driven recomputation with early cutoff: work stops as soon as a result comes out unchanged. When you ask why gen behaves a certain way, the answer leads back to the literature as well as the source.
Each of gen’s libraries is its own flake, and you can depend on any one of them alone. Take the
graph library for your own data, with no module system attached, or take the module system without
aspects. A flake that reads inputs.gen-graph.lib never touches the gen hub. The hub is a
convenience that gathers every library under one name.
Every guarantee gen makes names the check that holds it. None of gen’s libraries uses nixpkgs.lib,
and a purity test in each library’s own suite fails the moment one does. Every library’s suite runs
on each push under upstream Nix, Determinate Nix and Lix. Full nixpkgs comes in at the edge you
choose: gen’s flake-parts module builds each NixOS system with gen.nixpkgs, which defaults to your
flake’s own nixpkgs input.
Here is one class, nixos, followed through two machines: web serves traffic and db holds the
data.
Both machines run SSH. db also exports metrics, and web runs nginx. In plain NixOS the wiring
looks like this:
nixosConfigurations = { web = nixpkgs.lib.nixosSystem { specialArgs = { addr = "10.0.0.2"; users = [ "deploy" ]; }; modules = [ ./ssh.nix ./nginx.nix ]; }; db = nixpkgs.lib.nixosSystem { specialArgs = { addr = "10.0.0.1"; users = [ "deploy" "dba" ]; }; modules = [ ./ssh.nix ./monitoring.nix ]; # monitoring.nix needs ssh.nix };};Three facts in that file live only in your head:
monitoring.nix needs ssh.nix. A machine that lists the first and forgets the second still
evaluates.addr and users reach every module through specialArgs, whether the module reads them or not.Each feature is an aspect, a named bundle of configuration. An aspect can include other aspects,
and each include is an edge in the graph. An aspect that needs to know its machine takes host as
an argument, and gen passes the machine in.
aspects.ssh.nixos = { host, ... }: { services.openssh.enable = true; services.openssh.settings.AllowUsers = host.users;};aspects.nginx.nixos.services.nginx.enable = true;aspects.monitoring.includes = [ "ssh" ];aspects.monitoring.nixos = { host, ... }: { services.prometheus.exporters.node.listenAddress = host.addr;};
hosts.web = { addr = "10.0.0.2"; users = [ "deploy" ]; aspects = [ "nginx" "ssh" ]; };hosts.db = { addr = "10.0.0.1"; users = [ "deploy" "dba" ]; aspects = [ "monitoring" ]; };db lists only monitoring, and SSH arrives through the include edge. Each machine’s NixOS
configuration comes out filled in with its own address and users (output formatted for reading):
$ nix eval --file machines.nix{ db.services = { openssh = { enable = true; settings.AllowUsers = [ "deploy" "dba" ]; }; prometheus.exporters.node.listenAddress = "10.0.0.1"; }; includesOf = { monitoring = [ "ssh" ]; nginx = [ ]; ssh = [ ]; }; web.services = { nginx.enable = true; openssh = { enable = true; settings.AllowUsers = [ "deploy" ]; }; };}machines.nixThis file runs as it is, with no flake and no nixpkgs. The last step evaluates each machine’s modules with gen’s own module system. In a real flake that step is NixOS’s system builder.
let gen = import (builtins.fetchGit "https://github.com/sini/gen") { }; m = gen.merge; t = m.types;
cnf.keySemantics.nixos.category = "class"; aspectSchema = gen.aspects.mkAspectSchema cnf;
declared = m.evalModuleTree { } [ (aspectSchema.mkAspectModule { }) { options.hosts = m.mkOption { type = t.attrsOf t.raw; }; } { aspects.ssh.nixos = { host, ... }: { services.openssh.enable = true; services.openssh.settings.AllowUsers = host.users; }; aspects.nginx.nixos.services.nginx.enable = true; aspects.monitoring.includes = [ "ssh" ]; aspects.monitoring.nixos = { host, ... }: { services.prometheus.exporters.node.listenAddress = host.addr; };
hosts.web = { addr = "10.0.0.2"; users = [ "deploy" ]; aspects = [ "nginx" "ssh" ]; }; hosts.db = { addr = "10.0.0.1"; users = [ "deploy" "dba" ]; aspects = [ "monitoring" ]; }; } ];
# The include edges, as graph data. `project` follows them itself. facts = gen.aspects.graphFacts cnf declared.config.aspects;
projected = gen.delivery.project { values = declared.config; inherit cnf; selectNodes = v: v.hosts; };
# Each host's `nixos` modules, with `host` bound to that host's record. realized = gen.delivery.realize { inherit projected; terminals.nixos = { modules, bindings, ... }: (m.evalModuleTree { } ([ { freeformType = t.lazyAttrsOf t.anything; } ] ++ map (mod: (gen.bind.wrap { bindings.host = bindings.node; } mod).module) modules)).config; };in{ inherit (facts) includesOf; inherit (realized.nixos) web db;}Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).