A Small Deployment
A small deployment runs two NixOS hosts: web, a web server, and db, a database. Both need
SSH and monitoring. Two operator accounts exist: deploy has admin rights on both hosts, and
dba has an account on the database host only. Each feature is written once as an aspect, a
named and reusable piece of configuration, and gen builds one NixOS system per host.
| host | aspects it asks for | aspects it gets | users |
|---|---|---|---|
web | webserver | webserver, ssh, monitoring, base | deploy (wheel) |
db | database | database, ssh, monitoring, base | deploy (wheel), dba |
- flake.nix
- aspect-cnf.nix
Directorygen-modules/
- deployment.nix
Directoryhardware/
- web.nix
- db.nix
The flake
Section titled “The flake”{ inputs = { gen.url = "github:sini/gen"; nixpkgs.follows = "gen/nixpkgs"; flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; };
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { imports = [ inputs.gen.flakeModules.default ]; systems = [ "x86_64-linux" ];
gen = { tree = ./gen-modules; aspectCnf = import ./aspect-cnf.nix; nodeRegistryPath = [ "hosts" ]; extraModules.web = [ ./hardware/web.nix ]; extraModules.db = [ ./hardware/db.nix ]; }; };}{ keySemantics.nixos.category = "class";}# hardware/web.nix (db.nix is the same){ nixpkgs.hostPlatform = "x86_64-linux"; boot.loader.grub.enable = false; fileSystems."/" = { device = "none"; fsType = "tmpfs"; };}The hub options are listed in Add gen to a Flake.
The tree
Section titled “The tree”{ config, genAspects, genMerge, genSchema, ... }:let inherit (genMerge) mkOption types; aspectSchema = genAspects.mkAspectSchema (import ../aspect-cnf.nix);
# Two kinds. schema = genSchema.evalSchema { schemaOption = aspectSchema.schemaOption; } [ { config.schema.host = { options.aspects = mkOption { type = types.listOf types.str; default = [ ]; }; }; config.schema.user = { options.hosts = mkOption { type = types.listOf types.str; default = [ ]; }; options.admin = mkOption { type = types.bool; default = false; }; }; } ];
# The users that list a host. usersOn = host: builtins.filter (u: builtins.elem host config.users.${u}.hosts) (builtins.attrNames config.users);
in{ imports = [ (aspectSchema.mkAspectModule { }) ];
# Two registries. options.hosts = genSchema.mkInstanceRegistry { } schema.host; options.users = genSchema.mkInstanceRegistry { } schema.user;
config = { hosts.web.aspects = [ "webserver" ]; hosts.db.aspects = [ "database" ];
users.deploy = { hosts = [ "web" "db" ]; admin = true; }; users.dba.hosts = [ "db" ];
aspects.base.nixos = { node, ... }: { networking.hostName = node.name; users.users = builtins.listToAttrs (map (u: { name = u; value = { isNormalUser = true; extraGroups = if config.users.${u}.admin then [ "wheel" ] else [ ]; }; }) (usersOn node.name)); system.stateVersion = "25.05"; };
aspects.ssh.includes = [ "base" ]; aspects.ssh.nixos.services.openssh.enable = true;
aspects.monitoring.includes = [ "base" ]; aspects.monitoring.nixos.services.prometheus.exporters.node.enable = true;
aspects.webserver.includes = [ "ssh" "monitoring" ]; aspects.webserver.nixos.services.nginx.enable = true;
aspects.database.includes = [ "ssh" "monitoring" ]; aspects.database.nixos.services.postgresql.enable = true; };}The result
Section titled “The result”nix eval .#nixosConfigurations --apply builtins.attrNames# [ "db" "web" ]
nix eval --json .#nixosConfigurations.web.config.services.nginx.enable# true
nix eval --json .#nixosConfigurations.db.config.services.prometheus.exporters.node.enable# true
nix eval --json .#nixosConfigurations.db.config.users.users.deploy.extraGroups# ["wheel"]
nix build .#nixosConfigurations.db.config.system.build.toplevelweb runs nginx and db runs PostgreSQL. Both run SSH and the Prometheus node exporter,
which they reach through includes. dba has an account on db only.
Each part, explained
Section titled “Each part, explained”| part of the example | what it is | read more |
|---|---|---|
hosts, users, aspects in one tree | nodes in one graph, related by aspects, hosts and includes | Your Configuration as a Graph |
aspects.webserver, includes = [ "ssh" "monitoring" ] | reusable features that include each other | Reusable Features |
mkInstanceRegistry, node.name | instances of a kind, each with a stable identity | Stable Identity for Everything |
mkOption, types.listOf, evalSchema | gen’s own typed module system | An Enhanced Module Type System |
{ node, ... }: in aspects.base.nixos | the host bound into the module by name | Automatic Argument Injection |
aspect-cnf.nix, nodeRegistryPath, nixosConfigurations | projection and realization through a terminal | Delivery |
Each host lists only the aspects it asks for. Delivery follows includes itself: a host
receives the content of its aspects and of everything they include, so web gets
webserver, ssh, monitoring and base. gen-aspects’ graphFacts
publishes the same includes relation as graph data for gen-graph queries. See Aspects.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).