den.aspects
den.aspects
Section titled “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 ]; };};Auto-generation
Section titled “Auto-generation”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.
den.ful
Section titled “den.ful”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; };};flake.denful
Section titled “flake.denful”Type: attrsOf raw
Raw flake output for publishing namespaces. Set automatically by
den.namespace; consumed by downstream flakes that import your aspects.
Aspect structure
Section titled “Aspect structure”An aspect is an attribute set. Keys are either class keys (matching a registered class), nested aspect keys, or one of these structural keys:
| Key | Type | Purpose |
|---|---|---|
<class> | module / config | Config merged into entities of that class |
includes | listOf provider | Providers (aspects, sub-aspects, functions) pulled into this aspect |
excludes | listOf provider | Aspects or policies excluded from this subtree |
provides / _ | submodule | Sub-aspect namespace (_ is an alias for provides) |
policies | policy registry | Named policy functions, activated by placing in includes |
meta | submodule | Attached metadata (handleWith, aspect-chain, collisionPolicy; guard/aspects for conditional aspects) |
classes | lazyAttrsOf raw | Class schemas declared by this aspect, merged into den.classes |
name / description | str | Aspect 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.
Reserved keys
Section titled “Reserved keys”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.
Static vs parametric includes
Section titled “Static vs parametric includes”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.
meta.handleWith
Section titled “meta.handleWith”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.
meta.aspect-chain
Section titled “meta.aspect-chain”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.hasAspectmust not touchconfig.resolvedduring policy dispatch (that is cyclic). Routingwhenover inline aspects through the compile-conditional path is what makeshasAspectpredicates safe to write. - A failed guard that never recovers leaves a tombstone (visible in traces/diagrams, contributes no modules).
Class module collision policy
Section titled “Class module collision policy”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.
Resolution levels
Section titled “Resolution levels”The policy is resolved from three levels, first match wins:
| Level | Where to set | Type / default | Scope |
|---|---|---|---|
| Aspect | meta.collisionPolicy on the aspect | nullOr (enum), default null | All class modules in that aspect |
| Entity | collisionPolicy on a single entity (e.g. den.hosts.<sys>.<name>.collisionPolicy) | nullOr (enum), default null | That entity’s class modules |
| Kind | collisionPolicy on the entity schema (den.schema.<kind>) | nullOr (enum), default null | All entities of that kind |
| Global | den.config.classModuleCollisionPolicy | enum, 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".
Strategy values
Section titled “Strategy values”The enum is the same at every level: "error", "class-wins",
"den-wins".
| Value | Behavior |
|---|---|
"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 |
Setting collision policy
Section titled “Setting collision policy”# Aspect level — meta.collisionPolicyden.aspects.my-aspect.meta.collisionPolicy = "den-wins";
# Entity level -- one hostden.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.classModuleCollisionPolicyden.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.
Cross-context forwarding
Section titled “Cross-context forwarding”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 hostsden.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.
Class resolution
Section titled “Class resolution”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:
- Collects all aspects referenced by the entity
- Compiles each aspect into an effectful computation (
fx.send "resolve") - Evaluates static includes and defers parametric includes until context widens
- Walks the policy graph to fan out into derived stages
- Extracts the class-specific config (e.g.,
nixosfor NixOS hosts) - Merges everything into the entity’s
evalModulescall