Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

v0.9.0 #177

vic announced in Announcements
Feb 20, 2026 · 0 comments
Discussion options

Den v0.9.0 — Declarative Context Definitions and new documentation website!

This release introduces den.ctx, a declarative system for defining how context (data) is transformed and which aspects are applied at every stage of the configuration pipeline.

What's Changed

New Contributors

Full Changelog: v0.8.0...v0.9.0


Context flows (our new dependency system)

Before this release, den.default served two purposes:

  • A place to define global or generic includes for host, user, and home entities.
  • The backbone of context propagation — moving data from { host } to { host, user } to { host, user } in the HM pipeline, etc.

As a consequence, den.default.includes was abused by many of us, including Den itself, because it was where context transformation happened [2]. This "dependency system" — parametric aspects installed unconditionally at den.default.includes — was hard to reason about, hard to document, and hard for people to understand.

The symptoms were duplicate configuration values, caused by lax parametric functions matching too many pipeline stages.

What den.ctx Provides

  • Keep den.default for what it's good at: global settings. You can still use den.default.includes, but there are better alternatives now.
  • Move the dependency system out of den.default.includes: those parametric aspects were not individually testable, and you couldn't change how data flows. They were Den's hardcoded backbone.
  • Declarative data stages: context transformations are now explicit. Given a host, you declare how to enumerate users, detect HM support, etc.
  • Named contexts: previously we identified contexts only by their attrNames{ host }, { host, user }. Now they have names: ctx.host, ctx.hm-host. Names allow different contexts with the same structural shape but different semantic guarantees.
  • Extensible context flows: one core principle of Den is not getting in your way. You can create alternative flows, or use Den purely as a library.

Context Transformations: Parse don't Validate principle

Named contexts carry semantic meaning beyond their structure. ctx.host { host } and ctx.hm-host { host } hold the same data, but hm-host guarantees that home-manager support was validated:

  • inputs.home-manager exists (or the host has a custom hm-module)
  • The host has at least one user with class = "homeManager"

You cannot obtain an hm-host context unless these conditions hold. This follows the transform-don't-validate principle.

How a Context Type Works

A context type has four components: desc, conf, includes, and into.

den.ctx.foo.desc = "The foo context requires { foo } data.";
den.ctx.foo.conf = { foo }: my-aspects.${foo.name};

When ctx.foo is applied — it works like a function taking { foo } — it locates the responsible aspect via conf. For example, ctx.foo { foo.name = "bar"; } uses my-aspects.bar. The aspect's owned config, static includes, and parametric includes matching { foo } all contribute to whatever ctx.foo is being used to configure.

Context types are independent of NixOS. Den can be used as a library for network topologies, declarative cloud infrastructure, or anything describable as data transformations.

How a NixOS Configuration Is Built

The initial data for nixosConfigurations.igloo is the host itself:

# Nothing NixOS-specific yet — just a graph of dependencies.
aspect = den.ctx.host {
 host = den.hosts.x86_64-linux.igloo;
};

The result of ctxApply is a new aspect that includes den.aspects.igloo plus the entire transformation chain — user enumeration, HM detection, defaults.

# This is where things enter the NixOS domain.
nixosModule = aspect.resolve { class = "nixos"; };
nixosConfigurations.igloo = lib.nixosSystem {
 modules = [ nixosModule ];
};

These two steps can be adapted for any class, for anything Nix-configurable.

Context Propagation

Context transformation is declarative. If your data fans out to other contexts, you specify the transformations using .into:

den.ctx.foo.conf = { foo }: ...;
den.ctx.moo.conf = { moo }: ...;
den.ctx.foo.into.moo = { foo }: lib.singleton { moo = deriveMoo foo; };

All <source>.into.<target> transformations are taken into account by ctxApply.

Why Lists?

Transformations have the type source → [ target ]. This enables:

  • Fan-out: one host produces many { host, user } contexts (map)
  • Conditional propagation: zero or one contexts (lib.optional)
  • Pass-through: identity transformation (lib.singleton)

For example, HM detection uses conditional propagation:

den.ctx.host.into.hm-host = { host }:
 lib.optional (isHmSupported host) { inherit host; };

Same data, but the named context guarantees validation passed.

Contexts as Aspect Cutting-Points

Contexts are aspect-like themselves. They have owned configs and .includes:

# Owned config — only for validated HM hosts:
den.ctx.hm-host.nixos.home-manager.useGlobalPkgs = true;
# Scoped includes — only for validated HM hosts:
den.ctx.hm-host.includes = [
 ({ host, ... }: { nixos.home-manager.backupFileExtension = "bak"; })
];

This is like den.default.includes but scoped — it only activates for hosts with validated home-manager support.

Extending the Context Flow

You can add new transformations to any existing context type:

den.ctx.hm-host.into.foo = { host }: [ { foo = host.name; } ];
den.ctx.foo.conf = { foo }: ...;
den.ctx.foo.includes = [ ({ foo, ... }: ...) ];

The module system merges these definitions. You can extend the pipeline without modifying any built-in file.

Custom Context Flows

Each host has a mainModule option that defaults to:

(den.ctx.host { host }).resolve { class = "nixos"; }

You can override mainModule to use a completely alternative context flow, independent of ctx.host. Custom flows can be designed and tested in isolation — Den's CI uses a funny.names class that has nothing to do with NixOS to verify context mechanics independently.

What Happened to den.default?

den.default stays and is still useful for truly global settings. The issue was abusing den.default.includes as the context propagation backbone.

Internal Changes

Previously, all host, user, and home aspects had:

includes = [ den.default ]

Now they no longer include den.default directly. Including den.default explicitly is discouraged.

How Defaults Are Applied Now

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 }

den.default is now an alias for den.ctx.default. The data that flows into den.default.includes comes from these declarative transformations, not from direct aspect inclusion.

Best Practices

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

den.default remains the right place for values that genuinely apply everywhere — like stateVersion. Use context-specific includes for anything that belongs to a particular pipeline stage.


This discussion was created from the release v0.9.0.
You must be logged in to vote

Replies: 0 comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
1 participant

AltStyle によって変換されたページ (->オリジナル) /