Skip to content

Custom Nix Classes

Den’s built-in classes (nixos, darwin, homeManager) map to well-known NixOS module systems. But you can define custom classes that forward their contents into a target submodule path on another class.

This is how Den implements:

  • The user class (routes to users.users.<name> on the OS, via den.lib.policy.route)
  • Home Manager integration (forwards homeManager to home-manager.users.<name>)
  • hjem integration (forwards hjem to hjem.users.<name>)
  • nix-maid integration (forwards maid to users.users.<name>.maid)

den.batteries.forward creates a new class by forwarding its module contents into a target path on an existing class:

{ host }:
den.batteries.forward {
each = lib.attrValues host.users;
fromClass = user: "user";
intoClass = user: host.class;
intoPath = user: [ "users" "users" user.userName ];
fromAspect = user: user.aspect;
}
ParameterDescription
each = itemsList of items to forward (typically [ user ] or [ true ])
fromClass = item: classThe custom class name to read from
intoClass = item: classThe target class to write into
intoPath = item: path or args: pathTarget attribute path in the target class
fromAspect = item: aspectThe aspect to read the custom class from

For the built-in user class, that looks like:

flowchart LR
  A["den.aspects.alice.user = { extraGroups = [ wheel ]; }"] --> C[("user class content")]
  C -- "forward / route: intoClass = host.class, intoPath = users.users.alice" --> N["nixos: users.users.alice.extraGroups = [ wheel ]"]

The user class (os-user.nix) forwards OS-level user settings to NixOS/nix-Darwin lightweight user-environment.

# Instead of:
den.aspects.alice.nixos = { pkgs, ... }: {
users.users.alice = {
packages = [ pkgs.hello ];
extraGroups = [ "wheel" ];
};
};
# You write:
den.aspects.alice.user = { pkgs, ... }: {
packages = [ pkgs.hello ];
extraGroups = [ "wheel" ];
};

The user class is automatically forwarded to users.users.<userName> on whatever OS class the host uses (NixOS or Darwin).

Suppose you want a container class that forwards into virtualisation.oci-containers.containers.<name>:

{ den, lib, ... }:
let
fwd = { host, user }:
den.batteries.forward {
each = lib.singleton user;
fromClass = _: "container";
intoClass = _: host.class;
intoPath = _: [ "virtualisation" "oci-containers" "containers" user.userName ];
fromAspect = _: user.aspect;
};
in {
den.schema.user.includes = [ fwd ];
}

Now any user aspect can use the container class:

den.aspects.alice.container = {
image = "nginx:latest";
ports = [ "8080:80" ];
};

Every entry in den.classes.<name> is a record of options, not just a marker:

FieldTypeDefaultDescription
descriptionstr—Human-readable description of the class domain
forwardTonullOr { class; path? }nullDefault routing target for the forward battery: when a forward omits intoClass/intoPath, the class’s forwardTo.class and forwardTo.path are used instead
parentPathnullOr (name -> path)nullFor classes whose members nest inside an enclosing config owner (e.g. home-manager inside a host): a function name -> attrpath locating a named member within that owner’s config — the same route the class’s content is delivered to. null for root classes that own a top-level config (nixos, darwin, …)
parentArgnullOr strnullFor nested classes, the module argument by which a member reaches the enclosing config owner (home-manager exposes the host config as osConfig). null for root classes

forwardTo lets a class declare its destination once, so forwards only name the source:

den.classes.custom = {
description = "Custom class with forwardTo";
forwardTo = { class = "nixos"; path = [ ]; };
};
# no intoClass / intoPath needed:
den.batteries.forward {
each = lib.singleton true;
fromClass = _: "custom";
fromAspect = _: myAspect;
};

parentPath/parentArg are what make nested classes work — the pipe layer uses them to resolve a producer’s config at its producing class + scope. Den’s own homeManager class registers both:

den.classes.homeManager.parentPath = userName: [ "home-manager" "users" userName ];
den.classes.homeManager.parentArg = "osConfig";

forward supports optional parameters for complex scenarios:

den.batteries.forward {
each = lib.singleton true; # <- true is the single item, mostly ignored.
fromClass = _item: "wsl";
intoClass = _item: host.class;
intoPath = _item: [ "wsl" ];
fromAspect = _item: lib.head aspect-chain;
# Only forward if target has wsl options
guard = { options, ... }: options ? wsl;
# Modify module args for the forwarded module
adaptArgs = args: { osConfig = args.config; };
# Custom module type for the forwarded submodule
adapterModule = { config._module.freeformType = lib.types.anything; };
}
ParameterDescription
guard = args: bool or item: mkIfOnly forward when this predicate returns true
adaptArgs = args: attrsTransform module arguments before forwarding
adapterModule = deferredModuleCustom module for the forwarded submodule type

Community-contributed classes (aliases, platform-specific hm classes, role matching, guarded forwards, an impermanence class) are collected on Custom Class Examples.

ContributeCommunitySponsor