Skip to content

Configure Aspects

Den automatically creates an aspect for each of your host/user/home defintions. For example, with the following definitions:

den.hosts.x86_64-linux.igloo.users = {
tux = {
classes = [ "homeManager" "hjem" ];
};
pingu = {
classes = [ "maid" ];
};
};

Den will create the following aspects for you, each with empty owned-config keys for the entity’s classes:

den.aspects.igloo = {
nixos = {}; # `host.class` was derived from the host platform
};
den.aspects.tux = {
homeManager = {};
hjem = {};
};
den.aspects.pingu = {
maid = {};
};

These are plain attrsets — the pipeline makes them context-aware automatically. See Parametric Aspects for how aspects receive host/user/home context.

In addition to per-class modules, an aspect has an includes list referencing other aspects, and can hold nested aspects under keys of its own:

{ den, ... }: {
den.aspects.laptop = {
# Owned configs per class
nixos = { pkgs, ... }: {
environment.systemPackages = [ pkgs.git ];
};
darwin.nix-homebrew.enable = true;
homeManager.programs.starship.enable = true;
# Dependencies
includes = [
den.batteries.primary-user
den.aspects.gaming.emulation
];
};
den.aspects.gaming = {
darwin = ...;
nixos = ...;
emulation = {
darwin = ...;
nixos = ...;
};
};
}

Attributes named after a Nix class (nixos, darwin, homeManager, or any custom class) are referred by this documentation as owned configs.

They are just a regular Nix module:

  • Attrset Module: plain and simple configuration.
  • Function Module: taking module arguments ({ config, pkgs, lib, ... }: { }).
# Attrset form
den.aspects.laptop.nixos.networking.hostName = "laptop";
# Function form
den.aspects.laptop.nixos = { pkgs, ... }: {
environment.systemPackages = [ pkgs.git ];
};

includes is a list of aspects used to declare dependencies between aspects.

Unlike other Nix libraries that use stringly-typed “tags” to define requirements, Den uses real aspect references, these can be type-checked by the Nix module system.

den.aspects.workstation.includes = [
# Reference another aspect (its full DAG is included)
den.aspects.dev-tools
];

It is important to note the three kinds of values that Den distinguishes as part of an includes list:

  1. Static (plain attribute set): { nixos.foo = ...; }
  2. Static (aspects leaf): {class, aspect-chain}: { ${class}.foo = ...; }
  3. Parametric (any other function): { host, user }: { ${host.class}.foo = ...; }

(1) and (2) are termed static aspects and are the terminal leafs of aspects DAG. (1) provides configuration unconditionally, and (2) gets access to the class that is being resolved and an aspect-chain that lead to the current aspect (most recent last).

(3) is a more interesting kind of aspect that is used by Den to pass host/user defitions into these functions, so they can inspect the host features and provide configuration accordingly.

Any key on an aspect that is not a class key or a structural key (includes, excludes, meta, policies, …) is a nested aspect, reachable as den.aspects.<aspect>.<name>:

den.aspects.tools.editors = {
homeManager.programs.helix.enable = true;
homeManager.programs.vim.enable = true;
};
# Used elsewhere:
den.aspects.alice.includes = [ den.aspects.tools.editors ];

Nested aspects can also be parametric functions:

den.aspects.alice.work-vpn = { host, user, ... }:
lib.optionalAttrs host.hasVpn {
nixos.services.openvpn.servers.work.config = "...";
};

host.hasVpn is an attribute you set on the host, not a built-in one; see Freeform attributes.

Older configurations write den.aspects.<aspect>.provides.<name> (or ._.<name>). That nesting still works and yields the same aspect at den.aspects.<aspect>.<name>, but it is legacy; write the direct form. The cross-entity keys to-users, to-hosts, and <host> on a user aspect (below) are the exception: they are read only from provides.

In den.aspects.<path>, every segment is a name you choose, nested to any depth. This is a valid aspect:

den.aspects.foo.bar.baz.nixos = { config, ... }: { /* ... */ };

Declaring an aspect does not apply it. It reaches a host only when the host’s resolution tree includes it, by one of these routes:

# 1. From the host's own aspect
den.aspects.<host>.includes = [ den.aspects.foo.bar.baz ];
# 2. From a policy that injects it, activated somewhere in the tree
den.policies.<policy> = { host, ... }: [ (den.lib.policy.include den.aspects.foo.bar.baz) ];
den.schema.host.includes = [ den.policies.<policy> ];
# 3. For every host, through the host kind
den.schema.host.includes = [ den.aspects.foo.bar.baz ];
# 4. Globally, for every host, user, and home
den.default.includes = [ den.aspects.foo.bar.baz ];

Including a parent does not include its children. To include all of an aspect’s direct children at once, include den.aspects.<aspect>._:

den.aspects.foo = {
nixos = { /* not included by foo._ */ };
bar.nixos = { /* included */ };
bar.baz.nixos = { /* not included: a grandchild */ };
};
den.aspects.<host>.includes = [ den.aspects.foo._ ];

<aspect>._ includes each depth-1 child aspect, including legacy provides children, and nothing else: not the aspect’s own class keys, and not grandchildren (use den.aspects.foo.bar._ for those). The same _ used as an attribute path, as in den.aspects.<aspect>._.<name>, is the legacy alias for provides.<name>.

Three special provides keys route configuration across entity boundaries. They fire automatically during the tree walk — no manual policy wiring needed:

den.aspects.igloo = {
# Deliver to ALL users on this host
provides.to-users = { user, ... }: {
homeManager.programs.helix.enable = user.name == "alice";
};
# Deliver to a specific user by name
provides.alice.homeManager.programs.vim.enable = true;
};
den.aspects.tux = {
# Deliver to ALL hosts this user lives on
provides.to-hosts = { host, ... }: {
nixos.programs.nh.enable = host.name == "igloo";
};
# Deliver to a specific host by name
provides.igloo.nixos.programs.emacs.enable = true;
};

den.default is a special aspect applied to every host, user, and home:

{
den.default = {
nixos.system.stateVersion = "25.11";
homeManager.home.stateVersion = "25.11";
includes = [
den.batteries.define-user
den.batteries.inputs'
];
};
}

To use flake inputs inside class modules (inputs as a module argument, an input’s NixOS module, home-manager extraSpecialArgs), see Flake inputs and specialArgs.

Aspects have a meta submodule that can be used to attach meta-data at the aspect-level.

This is useful for introspection, for example, using some key to filter out aspects or communicate information about aspects to other tooling like graph generators.

Since .meta is a freeformType you can add any custom attribute or import a module on it.

The following are default meta.* attributes defined by Den:

aspect.name # "igloo", the submodule name
aspect.meta.loc # the location [ "den" "aspects" "igloo" ] from where name is derived.
aspect.meta.name # "den.aspects.igloo" derived from loc
aspect.meta.file # the last file where this aspect was defined.
aspect.meta.self # a reference to the aspect module `config`.

You can access meta values by referencing an aspect:

den.aspects.igloo.meta.name

Or if an aspect needs to access its own meta data as part of its parametric functor, it can do something like:

den.aspects.foo =
{ config, lib, ... }: # module args to access `config.meta`
{
meta.foo = "bar";
__functor = self: ctx:
if config.meta.foo == "bar"
then { } # do nothing, skip.
else {
includes = lib.filter (i: someCondition i) self.includes;
};
}

Beyond freeform metadata, meta carries pipeline-meaningful fields — handleWith (constraints: exclude/substitute/filter aspects in the subtree), collisionPolicy (flat-form class modules), and guard/aspects (conditional aspects). See Structural Introspection & Constraints and the aspects reference.

In case an aspect needs a custom submodule, it can be added this way:

# User aspect `tux`
den.aspects.tux =
{ config, ... }: # The function-args style is required for `imports` not to be interpreted as an Aspect class.
{
imports = [
{
options = {
default-key = lib.mkOption {
type = with lib.types; str;
};
keygrip = lib.mkOption {
type = with lib.types; str;
};
};
}
];
default-key = "Hello World!";
keygrip = "Hello World!";
}

And then, you can refer to tux aspect from other aspects:

{ den, ... }:
{
den.aspects.igloo = {
homeManager =
{ config, ... }:
{
programs = {
gpg = {
enable = true;
settings.default-key = den.aspects.tux.default-key; # Access the `tux` aspect statically
};
};
services = {
gpg-agent = {
enable = true;
enableSshSupport = true;
sshKeys = [
den.aspects.${config.home.username}.keygrip # Access the current user aspect dynamically
];
};
};
};
};
}

Aspects are fixed-point, meaning that they can reference themselves, as such:

{
den.aspects.igloo =
{config,...}: # Here, `config` represents the aspect `igloo` itself.
{
meta = {
default-key = "Hello World!";
};
homeManager = {
programs = {
gpg = {
enable = true;
settings.default-key = config.meta.default-key;
};
git = {
enable = true;
settings.signing.key = config.meta.default-key;
};
};
};
};
}
ContributeCommunitySponsor