Skip to content

Core Principles

Aspects and Contexts are Den core arquitectural principles.

It is how Den supports different platform configurations, packages/checks flake outputs, different home environments suppport, wsl support, third-party flake-parts perSystem modules, microvm guests, and pretty much everything.

It is also how more complex entities, like machine fleets, terraform or cloud infra can be configured with Den.

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
# A context transformation pipeline takes initially {host}
# and traverses its topology (host->[users]->[homes]) aggregating deps
aspect = den.ctx.host { host = den.hosts.x86_64-linux.my-laptop; };
# obtain the final module for nixos class
nixosModule = den.lib.aspects.resolve "nixos" aspect;
# Use NixOS API to instantiate or mix-in with other custom modules
nixosConfigurations.my-laptop = lib.nixosConfiguration { modules = [ nixosModule ]; };

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 den.ctx.<name>.provides.<name> as Aspect Pointcuts where configuration is applied to data in that context stage. Say you have a data shape: { x }, that you name as a foo context stage:

# This says how to move from one context stage `foo` into another stage `bar`
den.ctx.foo.into.bar = { x }: [ { y = x; } ]

The following den.ctx.bar.provides.bar is the actual pointcut that locates the aspect responsible for configuring using data { y } available at bar context stage.

# inlined aspect for this example, but can locate the aspect by any means
den.ctx.bar.provides.bar = { y }: { nixos.something = y; }

and since den.ctx.bar is itself an aspect, it can be used to include additional configurations at that particular stage:

den.ctx.bar.nixos.something-else = true;
den.ctx.bar.includes = [ den.aspects.other ];

This is how everything works in Den, the following is the context transitions that happen when applying den.ctx.host { host }

# host configuration: THIS is where your host-aspect is hooked into the pipeline
den.ctx.host.provides.host = { host }: den.aspects.${host.aspect};
# host -> users context transformation
den.ctx.host.into.user = { host }: map (user: { inherit host user; }) (lib.attrValues host.users); # transition into many { host, user }
# user configuration: Lookup user aspect.
den.ctx.user.provides.user = { host, user }: den.aspects.${user.aspect}; # Hook for the user-aspect.
# conditional transition ONLY if hm is enabled for user and host.
den.ctx.user.into.hm-user = { host, user }: lib.optional (host.hm.enable && lib.elem "homeManager" user.classes) { inherit host user; }
# host -> wsl-host: Same data shape, different context stage.
den.ctx.host.into.wsl-host = { host }: lib.optional host.wsl.enable { inherit host; }

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

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 (schema) from what it does (aspects):

LayerPurposeExample
SchemaDeclare entitiesden.hosts.x86_64-linux.laptop.users.alice = {}
AspectsConfigure behaviorden.aspects.laptop.nixos.networking.hostName = "laptop"
ContextTransform data flowden.ctx.host produces {host}, then {host, user} per user
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