From an Aspect to a NixOS System
This guide builds two NixOS hosts from nothing: web, a web server, and db, a database.
Both need SSH and monitoring. You declare two kinds, host and user, a few aspects that
carry NixOS configuration, and the hosts that use them. The hub turns each host into a
nixosConfiguration.
An aspect is a named, reusable feature. It holds configuration for one or more classes,
such as nixos, and can include other aspects. See Reusable Features.
The finished project has five files:
- flake.nix
- aspect-cnf.nix
Directorygen-modules/
- deployment.nix
Directoryhardware/
- web.nix
- db.nix
-
Wire the flake
Section titled “Wire the flake”flake.nix {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" ]; # (1)extraModules.web = [ ./hardware/web.nix ]; # (2)extraModules.db = [ ./hardware/db.nix ];};};}- Each member of the
hostsregistry becomes one NixOS system. - Per-host NixOS modules. Here they hold the platform and file systems:
# hardware/web.nix (db.nix is the same){nixpkgs.hostPlatform = "x86_64-linux";boot.loader.grub.enable = false;fileSystems."/" = { device = "none"; fsType = "tmpfs"; };}Add gen to a Flake lists every hub option.
- Each member of the
-
Declare the class
Section titled “Declare the class”aspect-cnf.nix {keySemantics.nixos.category = "class";}This file says that an aspect’s
nixoskey holds NixOS modules. The hub reads it to decide what to deliver, and the tree reads it to build the aspect schema. Both import the same file so they always agree. -
Declare the kinds
Section titled “Declare the kinds”A kind is a typed record type. Every
hostand everyuseris an instance of one. The rest of this step and the next ones are ingen-modules/deployment.nix.{ config, genAspects, genMerge, genSchema, ... }: # (1)letinherit (genMerge) mkOption types;aspectSchema = genAspects.mkAspectSchema (import ../aspect-cnf.nix);schema = genSchema.evalSchema { schemaOption = aspectSchema.schemaOption; } [ {config.schema.host = {options.aspects = mkOption { type = types.listOf types.str; default = [ ]; }; # (2)};config.schema.user = {options.hosts = mkOption { type = types.listOf types.str; default = [ ]; };options.admin = mkOption { type = types.bool; default = false; };};} ];- The hub passes these gen libraries to every module in the tree.
- The hub delivers the aspects named in each host’s
aspectslist.
-
Declare the registries and instances
Section titled “Declare the registries and instances”in{imports = [ (aspectSchema.mkAspectModule { }) ]; # (1)options.hosts = genSchema.mkInstanceRegistry { } schema.host; # (2)options.users = genSchema.mkInstanceRegistry { } schema.user;config = {hosts.web.aspects = [ "webserver" ]; # (3)hosts.db.aspects = [ "database" ];users.deploy = { hosts = [ "web" "db" ]; admin = true; };users.dba.hosts = [ "db" ];- Declares the
aspectsoption, built from the class declaration in step 2. - A registry holds the instances of one kind. Each instance gets its
nameand a stable identity. See Stable Identity for Everything. - A host lists only the aspects it asks for. Delivery adds everything they include.
- Declares the
-
Write the aspects
Section titled “Write the aspects”aspects.base.nixos = { node, ... }: { # (1)networking.hostName = node.name;users.users = builtins.listToAttrs (map (u: { # (2)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" ]; # (3)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;};}nodeis the host instance being built. gen passes it in by name, so the same aspect serves every host. See Automatic Argument Injection.- A query over the
usersregistry: the users that list this host. sshincludesbase, so any host withsshalso getsbase.
The helper goes in the
letblock from step 3:usersOn = host:builtins.filter (u: builtins.elem host config.users.${u}.hosts)(builtins.attrNames config.users);Delivery follows
includes: a host receives the content of the aspects it lists and of everything they include, sowebgetswebserver,ssh,monitoringandbase. -
Evaluate and build
Section titled “Evaluate and build”Terminal window nix eval .#nixosConfigurations --apply builtins.attrNames# [ "db" "web" ]nix eval --json .#nixosConfigurations.db.config.services.openssh.enable# truenix eval --json .#nixosConfigurations.db.config.users.users.deploy.extraGroups# ["wheel"]nix build .#nixosConfigurations.db.config.system.build.topleveldbruns PostgreSQL fromdatabase, and SSH, the node exporter and its host name fromssh,monitoringandbase, which it reaches through includes. It has accounts fordeployanddba.webruns nginx and has an account fordeployonly.
What happened
Section titled “What happened”- The hub merged
gen-modules/with gen’s own module system and produced the composed values:hosts,usersandaspects. - For each member of
hosts, it collected thenixoscontent of every aspect in that host’saspectslist. The class declaration inaspect-cnf.nixdecided which keys count as content to deliver. - It passed each host’s modules, with
nodebound to the host, plus itsextraModules, tonixpkgs.lib.nixosSystem. The result isnixosConfigurations.<host>.
A framework such as den hides most of this file behind its own vocabulary. The kinds, registries, aspects and delivery underneath are the same.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).