Core Principles
Den has four core concepts. Each has one job:
| Concept | What it is | Where it lives |
|---|---|---|
| Entity | A typed data record — a host, user, or home | den.hosts, den.homes, den.schema |
| Aspect | A composable unit of configuration that spans Nix classes | den.aspects |
| Policy | A function that defines how entities relate and route data | den.policies |
| Quirk | Structured data emitted by aspects, aggregated via pipes | den.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.
Den is Aspect-oriented
Section titled “Den is Aspect-oriented”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 is Context-driven
Section titled “Den is Context-driven”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` entitiesden.aspects.bar-config = { y }: { nixos.something = y; };
# Activate it for all `bar` entitiesden.schema.bar.includes = [ den.aspects.bar-config den.aspects.other ];Host resolution pipeline
Section titled “Host resolution pipeline”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 hostsden.schema.host.includes = [ ({ host }: den.aspects.${host.aspect}) ];
# host -> users: fan-out policyden.policies.host-to-users = { host, ... }: map (user: policy.resolve.to "user" { inherit host user; }) (lib.attrValues host.users);
# Activate user aspectsden.schema.user.includes = [ ({ host, user }: den.aspects.${user.aspect}) ];
# conditional: only if HM is enabled for user and hostden.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 kindden.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.
Feature-First, Not Host-First
Section titled “Feature-First, Not Host-First”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.
Context-Driven Dispatch
Section titled “Context-Driven Dispatch”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.
Composition via Includes
Section titled “Composition via Includes”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;};Separation of Concerns
Section titled “Separation of Concerns”Den separates what exists from what it does:
| Layer | Purpose | Example |
|---|---|---|
| Schema | Declare entities | den.hosts.x86_64-linux.laptop.users.alice = {} |
| Aspects | Configure behavior | den.aspects.laptop.nixos.networking.hostName = "laptop" |
| Policies | Entity topology and routing | den.policies.host-to-users fans out from hosts to users |
| Quirks | Structured data flow | den.quirks.firewall + pipe.collect aggregates across hosts |
| Batteries | Reusable patterns | den.provides.primary-user, den.provides.user-shell |
This separation means you can reorganize files, rename aspects, or add platforms without restructuring your configuration logic.