Context Pipeline
import { Aside } from ‘@astrojs/starlight/components’;
The Full Pipeline
Section titled “The Full Pipeline”When Den evaluates a host configuration, data flows through a pipeline of context transformations. Here is the complete picture:
graph TD
Host["den.hosts.x86_64-linux.igloo"]
Host -->|"creates"| CtxHost["ctx.host { host }"]
CtxHost -->|"conf"| HA["den.aspects.igloo<br/>(fixedTo { host })"]
CtxHost -->|"into.default"| CtxDef1["ctx.default { host }"]
CtxHost -->|"into.user<br/>(per user)"| CtxUser["ctx.user { host, user }"]
CtxHost -->|"into.hm-host<br/>(if HM detected)"| CtxHM["ctx.hm-host { host }"]
CtxUser -->|"conf"| UA["den.aspects.tux<br/>(fixedTo { host, user })"]
CtxUser -->|"into.default"| CtxDef2["ctx.default { host, user }"]
CtxHM -->|"conf"| HMmod["Import HM module"]
CtxHM -->|"into.hm-user<br/>(per HM user)"| CtxHMU["ctx.hm-user { host, user }"]
CtxHMU -->|"forward homeManager<br/>into host"| FW["home-manager.users.tux"]
CtxDef1 -->|"includes"| DI1["den.default.includes<br/>(host-context funcs)"]
CtxDef2 -->|"includes"| DI2["den.default.includes<br/>(user-context funcs)"]
How a nixosConfiguration Is Built
Section titled “How a nixosConfiguration Is Built”Here is the concrete path from a host declaration to a final NixOS configuration:
# 1. The initial data is the host itself — nothing NixOS-specific yet.aspect = den.ctx.host { host = den.hosts.x86_64-linux.igloo;};
# 2. ctxApply produces an aspect that includes den.aspects.igloo# plus the entire transformation chain (users, HM, defaults).
# 3. We enter the NixOS domain by resolving for the "nixos" class.nixosModule = aspect.resolve { class = "nixos"; };
# 4. Standard nixosSystem with the resolved module.nixosConfigurations.igloo = lib.nixosSystem { modules = [ nixosModule ];};This same pattern works for any class — replace "nixos" with
"darwin", "homeManager", or any custom class name.
Stage by Stage
Section titled “Stage by Stage”1. Host Entry
Section titled “1. Host Entry”Den reads den.hosts.x86_64-linux.igloo and creates the initial context:
ctx.host { host = den.hosts.x86_64-linux.igloo; }2. Host Aspect Resolution
Section titled “2. Host Aspect Resolution”ctx.host.conf locates den.aspects.igloo and fixes it to the host context.
All owned configs and static includes from the host aspect are collected.
3. Default Context (host-level)
Section titled “3. Default Context (host-level)”ctx.host.into.default produces { host } for ctx.default, which
activates den.default.includes functions matching { host, ... }.
4. User Enumeration
Section titled “4. User Enumeration”ctx.host.into.user maps over host.users, producing one
ctx.user { host, user } per user.
5. User Aspect Resolution
Section titled “5. User Aspect Resolution”ctx.user.conf locates both the user’s aspect (den.aspects.tux) and the
host’s aspect, collecting contributions from both directions.
6. Default Context (user-level)
Section titled “6. Default Context (user-level)”ctx.user.into.default activates den.default.includes again, this time
with { host, user } — functions needing user context now match.
7. Home-Manager Detection
Section titled “7. Home-Manager Detection”ctx.host.into.hm-host checks if the host has users with homeManager
class and a supported OS. If so, it activates ctx.hm-host.
8. HM Module Import
Section titled “8. HM Module Import”ctx.hm-host.conf imports the Home-Manager NixOS/Darwin module.
9. HM User Forwarding
Section titled “9. HM User Forwarding”For each HM user, ctx.hm-user uses den._.forward to take
homeManager class configs and insert them into
home-manager.users.<name> on the host.
Home-Manager Detection Criteria
Section titled “Home-Manager Detection Criteria”ctx.host.into.hm-host does not always activate. It checks three conditions
(see hm-os.nix):
- OS class is supported — the host’s class is
nixosordarwin - HM users exist — at least one user has
class = "homeManager" - HM module available —
inputs.home-managerexists, or the host has a customhm-module
All three must be true. Hosts without users, or with only non-HM users, skip the entire HM pipeline.
Duplication Caveat with den.default
Section titled “Duplication Caveat with den.default”den.default (alias for den.ctx.default) is included at every
context stage — once for the host context and once per user context.
This means:
- Owned configs and static includes from
den.defaultcan appear multiple times in the final configuration - For
mkMerge-compatible options (most NixOS options), this is harmless - For list options, you may get duplicate entries
To avoid duplication, use den.lib.take.exactly to restrict which
context stages a function matches:
den.default.includes = [ (den.lib.take.exactly ({ host }: { nixos.x = 1; }))];This function runs only in the { host } context, not in { host, user }.
Standalone Home-Manager
Section titled “Standalone Home-Manager”For den.homes, the pipeline is shorter:
graph TD
Home["den.homes.x86_64-linux.tux"]
Home -->|"creates"| CtxHome["ctx.home { home }"]
CtxHome -->|"conf"| HomeA["den.aspects.tux<br/>(fixedTo { home })"]
CtxHome -->|"into.default"| CtxDef["ctx.default { home }"]
den.default Is an Alias
Section titled “den.default Is an Alias”den.default is an alias for den.ctx.default. When you write:
den.default.homeManager.home.stateVersion = "25.11";den.default.includes = [ den._.define-user ];You are actually setting den.ctx.default.homeManager... and
den.ctx.default.includes. This means den.default is a full
context type — it has conf, into, includes, and owned attributes.
How den.default Receives Data
Section titled “How den.default Receives Data”Host, user, and home aspects do not include den.default directly.
Instead, each context type transforms into default:
den.ctx.host.into.default = lib.singleton; # passes { host }den.ctx.user.into.default = lib.singleton; # passes { host, user }den.ctx.home.into.default = lib.singleton; # passes { home }This means den.default is reached through the declarative context
pipeline, not by direct inclusion. The data flowing into den.default
is whatever the source context provides.
Best Practices
Section titled “Best Practices”den.default is useful for global settings like home.stateVersion.
However, prefer attaching parametric includes to the appropriate
context type instead:
| Instead of | Use |
|---|---|
den.default.includes = [ hostFunc ] | den.ctx.host.includes = [ hostFunc ] |
den.default.includes = [ hmFunc ] | den.ctx.hm-host.includes = [ hmFunc ] |
den.default.nixos.x = 1 | den.ctx.host.nixos.x = 1 |
Why This Design?
Section titled “Why This Design?”Each context type is independent and composable. You can:
- Add new context types without modifying existing ones
- Attach aspects to any stage of the pipeline
- Create custom transformations for domain-specific needs
- Override any built-in context behavior
The pipeline is not hardcoded — it’s declared through den.ctx definitions
that you can inspect, extend, and customize.