skip to content

Declare it once. Ask it anything.

gen weaves every layer of what you run, from the code you build on to the machines, clusters and clouds you ship to, into one living graph. It works the same on a single laptop as on thousands of servers.

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.

Features are written once

A feature takes its machine as an argument, and gen passes the machine in. Reusable Features

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.

DestinationWhat gen does there
Linux servers, with NixOSHands 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-managerHands 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 nixidynixidy 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 terranixterranix 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 systemgen-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.
  • The module lists are the only record of which machine has which feature. To answer “which machines run SSH?” you read every list.

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):

Terminal window
$ 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" ]; };
};
}
The whole of machines.nix

This 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;
}
palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).