From Den
Den and yen share the aspect idea. The difference is where structure comes from.
Den gives you a schema: den.hosts, users, homes and context pipes. In yen you declare those kinds yourself, in
plain Nix, and every rule of how content moves is a sink you can read.
This page moves Den’s default template to yen. The docs evaluate both and check that the host gets the same account, groups, home, packages and state versions.
| Den | yen |
|---|---|
den.hosts.x86_64-linux.igloo.users.tux | instances: yen.host.igloo, yen.user.tux with hosts = [ … ] |
den.aspects.igloo.nixos | an aspect with at = yen.host.igloo and a nixos key |
den.default.nixos | an aspect with at = yen.kinds.host |
includes = [ den.batteries.x ] | the included aspect’s at names the includer, from any file |
provides.to-users.homeManager | an aspect at the users within the host |
hasAspect | yen.lib.holds, and r.graph edges with origin |
context pipes, osConfig | args: a kind, a sink or out (the built flake) |
den.schema.* | yen.kinds.<kind>.fields |
The template, side by side
Section titled “The template, side by side”{ inputs, lib, ... }:{ imports = [ inputs.den.flakeModule ]; den.schema.user.classes = lib.mkDefault [ "homeManager" ]; den.default.nixos.system.stateVersion = "25.05"; den.default.homeManager.home.stateVersion = "25.05"; den.hosts.x86_64-linux.igloo.users.tux = { };}# What Den gives you built in, declared: hosts build NixOS, users live on hosts, a user's home goes into its host.{ yen, lib, inputs, ...}:{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.host = { fields.system = lib.types.str; spawns.user = s: lib.filterAttrs (_: u: lib.any (yen.lib.same s) u.hosts) yen.user; sinks.nixos.out = { host, nixos, ... }: { nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { inherit (host) system; modules = nixos; }; }; }; yen.kinds.user = { fields.hosts = yen.lib.is lib.isList; sinks.homeManager.nixos = { user, homeManager, ... }: { home-manager.users.${user.name}.imports = homeManager; }; }; yen.aspects.home-manager = { at = yen.lib.has yen.kinds.user; nixos.imports = [ inputs.home-manager.nixosModules.home-manager ]; };
yen.fleet.main = { }; yen.flake = { out, ... }: out;}Den’s schema is built in. In yen, fleet.nix says the same in one short file: hosts build NixOS, users live under their
hosts, a user’s homeManager content goes into the host, and home-manager is active where a user lives.
{ den.aspects.igloo = { nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.hello ]; boot.loader.grub.enable = false; fileSystems."/" = { device = "/dev/fake"; fsType = "auto"; }; }; provides.to-users.homeManager = { pkgs, ... }: { home.packages = [ pkgs.vim ]; }; };}# den.hosts.x86_64-linux.igloo.users.tux, and the igloo aspect.{ yen, ... }:{ yen.host.igloo.system = "x86_64-linux"; yen.aspects.igloo = { at = yen.host.igloo; nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.hello ]; boot.loader.grub.enable = false; fileSystems."/" = { device = "/dev/fake"; fsType = "auto"; }; }; }; # provides.to-users: content for the users of igloo, given at each user yen.aspects.igloo-users = { at = yen.lib.all [ yen.kinds.user (yen.lib.within yen.host.igloo) ]; homeManager = { pkgs, ... }: { home.packages = [ pkgs.vim ]; }; };}{ den, ... }:{ den.aspects.tux = { includes = [ den.batteries.define-user den.batteries.primary-user ]; homeManager = { pkgs, ... }: { home.packages = [ pkgs.htop ]; }; };}# The tux aspect. `includes` becomes `at` on the included aspects.{ yen, ... }:{ yen.user.tux.hosts = [ yen.host.igloo ]; yen.aspects.define-user.at = yen.user.tux; yen.aspects.primary-user.at = yen.user.tux; yen.aspects.tux = { at = yen.user.tux; homeManager = { pkgs, ... }: { home.packages = [ pkgs.htop ]; }; };}Batteries are aspects
Section titled “Batteries are aspects”A battery is an aspect that you include. In yen you write the aspect once, without at, and each user of it
adds itself to at. at from several files is a union.
# den.batteries.define-user: an aspect. Who includes it is its `at`, from any file.{ yen, ... }:{ yen.aspects.define-user = { nixos = { user, ... }: { users.users.${user.name} = { name = user.name; home = "/home/${user.name}"; isNormalUser = true; }; }; homeManager = { user, ... }: { home.username = user.name; home.homeDirectory = "/home/${user.name}"; }; };}{ yen, ... }:{ yen.aspects.primary-user.nixos = { user, ... }: { users.users.${user.name}.extraGroups = [ "wheel" "networkmanager" ]; };}# den.default: an aspect at every scope of a kind.{ yen, ... }:{ yen.aspects.state-version = { at = yen.kinds.host; nixos.system.stateVersion = "25.05"; }; yen.aspects.home-state-version = { at = yen.kinds.user; homeManager.home.stateVersion = "25.05"; };}Quirks and pipes are sinks
Section titled “Quirks and pipes are sinks”In Den, a quirk is structured data that aspects emit on a key registered in den.quirks, and pipes are policy effects
that route it: pipe.expose up to the host, pipe.collect across hosts, pipe.filter, pipe.as. By default a quirk stays
in its own scope. From Den’s docs:
den.quirks.firewall = { description = "Firewall port declarations"; };den.aspects.nginx = { firewall = { ports = [ 80 443 ]; }; };den.aspects.networking.nixos = { firewall, lib, ... }: { networking.firewall.allowedTCPPorts = lib.concatMap (f: f.ports or []) firewall;};In yen a quirk is a sink. Where its values land is the tree: an element goes up to the nearest scope that has the sink.
A user’s ports reach its host with no policy, which is Den’s pipe.expose. entries keeps who gave each value, so a
filter or a rename is a plain function in the reader:
# Den quirks and pipes, in yen: a quirk is a sink; where it lands is the tree, not a policy.{ yen, lib, ... }:{ # den.quirks.firewall: a sink on the host. A user has no `firewall` sink, so a user's ports go up # to its host by routing: Den's `pipe.expose` is the default here. yen.kinds.host.sinks.firewall = { reduce = yen.lib.reduce.entries; # each value with the scope that gave it: provenance, as in pipe.collect nixos = { firewall, ... }: { networking.firewall.allowedTCPPorts = lib.unique (lib.concatMap (e: e.value.ports) firewall); }; };
# producers do not know the consumer: they give `firewall` elements yen.aspects.ssh = { at = yen.kinds.host; firewall.ports = [ 22 ]; }; yen.aspects.tux-dev-server = { at = yen.user.tux; firewall.ports = [ 3000 ]; # from a user, lands on each of its hosts };
# pipe.filter / pipe.as: a plain reader. Here: the ports that users opened, for a report yen.kinds.host.sinks.firewall.out = { host, firewall, ... }: { userPorts.${host.name} = lib.concatMap (e: e.value.ports) ( lib.filter (e: e.from.kind == "user") firewall ); };}| Den | yen |
|---|---|
den.quirks.<q> | a sink <q> on the kind that consumes it |
| emit on a quirk key | an aspect key with the sink’s name |
consume with { firewall, ... }: | the same: a sink is an arg |
pipe.expose (user to host) | routing: the user has no such sink, its host has one |
pipe.collect, pipe.broadcast | a sink at a common ancestor (the fleet), read by each host |
pipe.filter, pipe.as | a function over the reduced value, with entries for provenance |
| a wall: a value that must not leave its scope | a sink with no keys |
Policies are spawns
Section titled “Policies are spawns”A Den policy is a function from context to effects, such as host-to-users that resolves each user under its host.
The policy graph (flake, system, host, user, home) is Den’s structure. In yen you write that structure as spawns:
yen.kinds.host.spawns.user in fleet.nix above is Den’s host-to-users. Conditional policies are a spawns
that reads data or yen.lib.holds. Include and exclude effects are at, and not.