Custom Nix Classes
What is a Custom Class
Section titled “What is a Custom Class”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
userclass (routes tousers.users.<name>on the OS, viaden.lib.policy.route) - Home Manager integration (forwards
homeManagertohome-manager.users.<name>) - hjem integration (forwards
hjemtohjem.users.<name>) - nix-maid integration (forwards
maidtousers.users.<name>.maid)
The forward Battery
Section titled “The forward Battery”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;}| Parameter | Description |
|---|---|
each = items | List of items to forward (typically [ user ] or [ true ]) |
fromClass = item: class | The custom class name to read from |
intoClass = item: class | The target class to write into |
intoPath = item: path or args: path | Target attribute path in the target class |
fromAspect = item: aspect | The 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 ]"]
Example: The Built-in user Class
Section titled “Example: The Built-in user Class”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).
Creating Your Own Class
Section titled “Creating Your Own Class”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" ];};Class Fields
Section titled “Class Fields”Every entry in den.classes.<name> is a record of options, not just a marker:
| Field | Type | Default | Description |
|---|---|---|---|
description | str | — | Human-readable description of the class domain |
forwardTo | nullOr { class; path? } | null | Default routing target for the forward battery: when a forward omits intoClass/intoPath, the class’s forwardTo.class and forwardTo.path are used instead |
parentPath | nullOr (name -> path) | null | For 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, …) |
parentArg | nullOr str | null | For 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";Advanced: Guards and Adapters
Section titled “Advanced: Guards and Adapters”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; };}| Parameter | Description |
|---|---|
guard = args: bool or item: mkIf | Only forward when this predicate returns true |
adaptArgs = args: attrs | Transform module arguments before forwarding |
adapterModule = deferredModule | Custom module for the forwarded submodule type |
More examples
Section titled “More examples”Community-contributed classes (aliases, platform-specific hm classes,
role matching, guarded forwards, an impermanence class) are collected on
Custom Class Examples.