Skip to content

Core Principles

Den has four core concepts. Each has one job:

ConceptWhat it isWhere it lives
EntityA typed data record — a host, user, or homeden.hosts, den.homes, den.schema
AspectA composable unit of configuration that spans Nix classesden.aspects
PolicyA function that defines how entities relate and route dataden.policies
QuirkStructured data emitted by aspects, aggregated via pipesden.quirks

Entities declare what exists. Aspects declare what it does. Policies declare how things relate. Quirks let aspects share structured data without coupling.

These four concepts compose to support NixOS, nix-Darwin, home-manager, WSL, MicroVM, flake-parts perSystem modules, machine fleets, and anything else configurable through Nix modules.

From our README header example:

# These three lines is how Den instantiates a configuration.
# Other Nix configuration domains outside NixOS/nix-Darwin
# can use the same pattern. demo: templates/nvf-standalone
# Den resolves entities declared in den.hosts automatically.
# Policies drive topology (host->[users]->[homes]).
# The pipeline collects all aspects and produces per-class modules.
# For manual resolution outside the pipeline:
aspect = den.lib.aspects.resolve "nixos" (den.aspects.my-aspect { host = den.hosts.x86_64-linux.my-laptop; });
nixosConfigurations.my-laptop = lib.nixosSystem { modules = [ aspect ]; };

Anything that you can describe via a data structure that can be traversed, can be configured like we do for NixOS.

Most importantly, the context {host,user} here are not _module.args nor specialArgs, it is an actual function, not a module-looking-as-a-function. This means config can depend on context without Nix infinite loops.

Den uses policies and schema includes as Aspect Pointcuts — where configuration is applied to data at a given point in the pipeline. Say you have an entity kind foo with data shape { x }:

# This policy fans out from entity kind `foo` to entity kind `bar`
den.policies.foo-to-bar = { x, ... }:
[ (policy.resolve.to "bar" { y = x; }) ];

Aspects activated on bar entities receive { y } in their context:

# Aspect that configures using data available when resolving `bar` entities
den.aspects.bar-config = { y }: { nixos.something = y; };
# Activate it for all `bar` entities
den.schema.bar.includes = [ den.aspects.bar-config den.aspects.other ];

This is how everything works in Den. Policies drive entity topology, and schema includes activate aspects at each entity kind:

# Activate the host's own aspect for all hosts
den.schema.host.includes = [ ({ host }: den.aspects.${host.aspect}) ];
# host -> users: fan-out policy
den.policies.host-to-users = { host, ... }:
map (user: policy.resolve.to "user" { inherit host user; })
(lib.attrValues host.users);
# Activate user aspects
den.schema.user.includes = [ ({ host, user }: den.aspects.${user.aspect}) ];
# conditional: only if HM is enabled for user and host
den.policies.user-to-hm-user = { host, user, ... }:
lib.optional (host.hm.enable && lib.elem "homeManager" user.classes)
(policy.resolve.to "hm-user" { inherit host user; });
# host -> wsl-host: same data shape, different entity kind
den.policies.host-to-wsl-host = { host, ... }:
lib.optional host.wsl.enable
(policy.resolve.to "wsl-host" { inherit host; });

people can define their own extensions to Den’s NixOS pipeline, or define other pipelines entirely.

Traditional Nix configurations start from hosts and push modules downward. Den follows a Dendritic model that inverts this: aspects (features) are the primary organizational unit. Each aspect declares its behavior per Nix class, and hosts simply select which aspects apply to them.

flowchart BT
  subgraph "Aspect: bluetooth"
    nixos["nixos: hardware.bluetooth.enable = true"]
    hm["homeManager: services.blueman-applet.enable = true"]
  end
  nixos --> laptop
  nixos --> desktop
  hm --> laptop
  hm --> desktop

An aspect consolidates all class-specific configuration for a single concern. Adding bluetooth to a new host is one line: include the aspect. Removing it is deleting that line.

Den uses function parametric dispatch: aspect functions declare which context parameters they need via their argument pattern.

# Runs in every context (host, user, home)
{ nixos.networking.firewall.enable = true; }
# Runs only when a {host} context exists
({ host }: { nixos.networking.hostName = host.hostName; })
# Runs only when both {host, user} are present
({ host, user }: {
nixos.users.users.${user.userName}.extraGroups = [ "wheel" ];
})

Den introspects function arguments at evaluation time. A function requiring { host, user } is silently skipped in contexts that only have { host }. No conditionals, no mkIf, no enable — the context shape is the condition.

Aspects form a directed acyclic graph through includes and form a tree of related aspects using provides.

den.aspects.workstation = {
includes = [
den.aspects.dev-tools
den.aspects.gaming.provides.emulation
den.provides.primary-user
];
nixos.services.xserver.enable = true;
};

Den separates what exists from what it does:

LayerPurposeExample
SchemaDeclare entitiesden.hosts.x86_64-linux.laptop.users.alice = {}
AspectsConfigure behaviorden.aspects.laptop.nixos.networking.hostName = "laptop"
PoliciesEntity topology and routingden.policies.host-to-users fans out from hosts to users
QuirksStructured data flowden.quirks.firewall + pipe.collect aggregates across hosts
BatteriesReusable patternsden.provides.primary-user, den.provides.user-shell

This separation means you can reorganize files, rename aspects, or add platforms without restructuring your configuration logic.

Contribute Community Sponsor