Skip to content

den.aspects

An attribute set of aspects. Each aspect key names an aspect; its value is an aspect set containing per-class deferred modules and optional includes and provides:

den.aspects = {
dns = {
nixos.services.resolved.enable = true;
darwin.networking.dns = [ "1.1.1.1" ];
includes = [ ./dns ];
};
};

Den auto-generates den.aspects entries from den.hosts, den.homes, and the users declared under each host’s users. For every declared host/home/user, an aspect is created with the appropriate class configurations. You do not need to declare den.aspects manually unless adding shared aspects.

Type: attrsOf namespaceType (internal)

Namespaced aspect collections. Each key is a namespace name, each value is a namespace container whose freeform keys are aspects (it also carries schema, classes, and _). Populated by den.namespace or by merging upstream denful flake outputs.

den.ful.myns = {
some-aspect = { nixos.services.foo.enable = true; };
};

Type: attrsOf raw

Raw flake output for publishing namespaces. Set automatically by den.namespace; consumed by downstream flakes that import your aspects.

An aspect is an attribute set. Keys are either class keys (matching a registered class), nested aspect keys, or one of these structural keys:

KeyTypePurpose
<class>module / configConfig merged into entities of that class
includeslistOf providerProviders (aspects, sub-aspects, functions) pulled into this aspect
excludeslistOf providerAspects or policies excluded from this subtree
provides / _submoduleSub-aspect namespace (_ is an alias for provides)
policiespolicy registryNamed policy functions, activated by placing in includes
metasubmoduleAttached metadata (handleWith, aspect-chain, collisionPolicy; guard/aspects for conditional aspects)
classeslazyAttrsOf rawClass schemas declared by this aspect, merged into den.classes
name / descriptionstrAspect name and description

The pipeline also attaches internal __-prefixed fields (e.g. __args on parametric aspects, __functor to make merged aspects callable). These are implementation details, not authoring surface.

Keys on an aspect that match a registered class, quirk, or nested-aspect shape are dispatched by the pipeline. Anything else is treated as a class key of an unregistered class (backwards compat). To keep your own metadata keys out of dispatch entirely, reserve them globally:

den.reservedKeys = [ "settings" "tags" ];

Reserved keys are structural: the pipeline ignores them, so you can carry arbitrary metadata on any aspect without it being interpreted as class content.

A plain module function in includes (module-system signature) is static — emitted once during aspect resolution. A function requesting scope context arguments ({ host }, { user }, { home }, etc.) is parametric — deferred and re-resolved per context as scope widens. See Parametric aspects.

Type: nullOr (handler record or list of handler records). Default: null.

Resolution handlers for the aspect’s subtree. The primary handler records are the constraints from den.lib.aspects.fx.constraints (exclude, substitute, filterBy) — see Structural Introspection & Constraints for the full API and examples.

Type: listOf str or null (internal, read-only). No default — a declared aspect sets its own chain.

Tracks the structural origin of an aspect as a path. A top-level aspect has meta.aspect-chain = []. An aspect provided by foo (via foo.provides.bar or its alias foo._.bar) has meta.aspect-chain = ["foo"]. Deeply nested providers accumulate: foo._.bar._.baz has meta.aspect-chain = ["foo" "bar"].

null and [] are different values. [] means “root — the chain is empty”; null means “no chain set”, which is what an inline includes literal carries until something fills it in. Absence, not root.

The chain distinguishes aspects by origin during pipeline resolution.

meta.guard / meta.aspects — conditional aspects

Section titled “meta.guard / meta.aspects — conditional aspects”

An aspect carrying meta.guard is conditional: instead of emitting its content immediately, the pipeline evaluates the guard against the in-flight aspect tree and only emits when it passes.

You almost never write these fields by hand — the user-facing entry point is policy.when applied to an inline aspect (or a policy.include effect):

den.aspects.igloo.includes = [
(den.lib.policy.when ({ host, ... }: host.hasAspect den.aspects.git) {
nixos.environment.variables.GIT_ENABLED = "true";
})
];

when wraps the inline aspect into a conditional (meta.guard = predicate; meta.aspects = [ … ]). Key semantics:

  • Guard context — the predicate receives hasAspect ref (bool) directly, plus entity-shaped stubs (host.hasAspect ref, user.hasAspect ref, …) for the kinds bound in the current scope. Membership is structural (the in-flight tree, scoped to this entity’s own subtree + ancestors — not fleet-wide) and exclude-aware (excluded aspects read as absent).
  • Deferred re-evaluation — if the guard fails, the conditional is parked and re-evaluated after siblings finish resolving. A guard on an aspect that a later include pulls in still passes (the classic “order shouldn’t matter” case).
  • Why this exists — a predicate that reads entity.hasAspect must not touch config.resolved during policy dispatch (that is cyclic). Routing when over inline aspects through the compile-conditional path is what makes hasAspect predicates safe to write.
  • A failed guard that never recovers leaves a tombstone (visible in traces/diagrams, contributes no modules).

A collision occurs when a flat-form class module declares an arg (e.g. host) that the module system also provides — via specialArgs or _module.args. The collision policy selects what happens. See Class Modules for the flat-form mechanism.

The policy is resolved from three levels, first match wins:

LevelWhere to setType / defaultScope
Aspectmeta.collisionPolicy on the aspectnullOr (enum), default nullAll class modules in that aspect
EntitycollisionPolicy on a single entity (e.g. den.hosts.<sys>.<name>.collisionPolicy)nullOr (enum), default nullThat entity’s class modules
KindcollisionPolicy on the entity schema (den.schema.<kind>)nullOr (enum), default nullAll entities of that kind
Globalden.config.classModuleCollisionPolicyenum, default "error"Entire flake

A null at any of the first three levels falls through to the next. The global level supplies the effective default of "error".

The enum is the same at every level: "error", "class-wins", "den-wins".

ValueBehavior
"error"Throw an eval error (effective default)
"den-wins"Den value takes precedence; a warning is emitted
"class-wins"Module-system value takes precedence; a warning is emitted
# Aspect level — meta.collisionPolicy
den.aspects.my-aspect.meta.collisionPolicy = "den-wins";
# Entity level -- one host
den.hosts.x86_64-linux.igloo.collisionPolicy = "class-wins";
# Kind level -- den.schema.<kind>.collisionPolicy (all hosts)
den.schema.host.collisionPolicy = "den-wins";
# Global — den.config.classModuleCollisionPolicy
den.config.classModuleCollisionPolicy = "den-wins";

den.batteries (aliased as den.provides and den._)

Section titled “den.batteries (aliased as den.provides and den._)”

This is the place for Den built-in batteries, reusable aspects that serve as basic utilities and examples.

See Batteries Reference.

Entities (hosts, users, homes) expose .resolved — the aspect produced by their context pipeline. Forwards can use this to pull configuration from one entity into another without manually wiring context calls.

When den.batteries.forward is called without fromAspect, it defaults to item.resolved, resolving the source entity through its own context pipeline:

# Collect SSH host keys from all other hosts
den.aspects.iceberg.includes = [
({ host }:
den.batteries.forward {
each = lib.filter (h: h != host) (lib.attrValues den.hosts.${host.system});
fromClass = _: "ssh-host-key";
intoClass = _: host.class; # "nixos" or "darwin"
intoPath = _: [ ];
}
)
];

Each host in each is resolved via its .resolved attribute, and the ssh-host-key class content is forwarded into the target’s OS config. host.class is the OS class name ("nixos", "darwin"), not the context type.

Resolution is handled internally by the fx pipeline. den.lib.aspects.resolve still exists but is not a public API — call it directly only if you are building custom pipeline stages.

When the pipeline resolves aspects for an entity, it:

  1. Collects all aspects referenced by the entity
  2. Compiles each aspect into an effectful computation (fx.send "resolve")
  3. Evaluates static includes and defers parametric includes until context widens
  4. Walks the policy graph to fan out into derived stages
  5. Extracts the class-specific config (e.g., nixos for NixOS hosts)
  6. Merges everything into the entity’s evalModules call
Contribute Community Sponsor