Date: 2026-05-27
Companion to: 2026-05-27-clan-gen-integration-analysis.md (what gen offers Clan)
This document: What den should borrow (functional gaps only), the bridging strategy, and the roadmap.
Den and Clan solve overlapping problems differently. Den has ~100s of users and strong formal foundations: scope graphs, algebraic composition, demand-driven evaluation, multi-class output, and the ability to materialize any output target (NixOS, darwin, homeManager, terraform, devshells, packages, treefmt, nix-unit, flake-parts perSystem) from a single aspect graph. Clan has a slightly larger user base and launched earlier, but forces NixOS module evaluation for every entity and can only target nixos/darwin.
Den's competitive advantages are real and hard to replicate:
- Multi-target materialization from one aspect graph (not one pipeline per target)
- Processing outside host-level evaluation — the scope graph resolves aspects, dedup, classification, and routing before any
lib.evalModulescall - O(subtree) not O(fleet) evaluation for single-host builds
- Class-agnostic composition — the same aspect key dispatch works for terraform, devshells, packages, and NixOS configs
What den should borrow from Clan is limited to functional gaps — capabilities den doesn't have at all — not implementation patterns.
Den's class system makes the aspect graph a universal materializer. A single aspect can emit into any registered output class:
# One aspect, multiple output targets
den.aspects.web-service = {
nixos.services.nginx.enable = true; # → nixosConfigurations
darwin.services.nginx.enable = true; # → darwinConfigurations
homeManager.programs.firefox.enable = true; # → home-manager config
terranix = { # → terraform config.tf.json
resource.hcloud_server.web.server_type = "cx21";
};
devshell = { pkgs, ... }: { # → nix develop shell
commands = [{ package = pkgs.curl; }];
};
packages.web-deploy = pkgs.writeShellScriptBin # → nix build
"deploy" "rsync ...";
tests.web-smoke = { /* nix-unit test */ }; # → nix-unit
treefmt.programs.nixfmt.enable = true; # → treefmt
files."README.md".text = "..."; # → file generation
};Each class is a den.classes.<name> registration + a route policy that determines where the collected modules end up:
# Register a class
den.classes.terranix = { };
# Policy: collect terranix modules from host subtree, route to flake output
den.policies.host-to-terranix = { host, ... }: [
(den.lib.policy.instantiate {
name = "${host.name}-tf";
class = "terranix";
instantiate = { modules, ... }: modules;
intoAttr = [ "terranixModules" host.name ];
})
];
# Feed into terranix's flake-module
perSystem = { pkgs, ... }: {
terranix.terranixConfigurations = lib.mapAttrs (_: modules: {
inherit modules;
}) (config.flake.terranixModules or { });
};This is den's greatest strength. The aspect pipeline processes all class keys through the same scope graph — dedup, classification, policy routing, pipe data flow — before any target-specific evaluation. Clan can't do this; their entire pipeline is coupled to lib.evalModules and the NixOS/darwin module system.
A Clan service that needs terraform resources requires a separate terranix integration outside the service framework. A den aspect just adds a terranix key.
These are capabilities den lacks entirely. Not implementation patterns — just the features.
Den has no secrets infrastructure. Every fleet needs one. Clan's vars system has the right feature set: generators with dependency ordering, pluggable backends (sops, age, in-repo), secret/public split, shared vs per-machine, file-level metadata.
What to build: A den.vars system that leverages the scope graph for visibility, gen-graph for dependency ordering, and gen-derive for backend selection.
Den thinks host-first ("this host gets nginx"). Real fleets also need service-first thinking ("borgbackup needs servers and clients, here's how they find each other"). The role concept — multiple entity subsets cooperating within a service — has no den equivalent.
What to build: A den.services sugar layer that translates roles → nested aspects, membership → selectors, cross-role discovery → scope graph import edges.
Den aspects are opaque attrsets. You can't query "what aspects are available?", "what does this need?", or "what does this provide?" without reading code. Clan's manifests (name, description, categories, exports.out/inputs, constraints) solve this.
What to build: aspect.meta with gen-schema introspection for external tooling.
Den discovers entities from den.hosts/den.homes declarations but has no fleet-level grouping. Clan's inventory provides tags, metadata, environments, and computed membership.
What to build: den.fleet with tags as scope graph node attributes, queryable via gen-select.
C++ succeeded partly because it compiled C code unchanged. The bridge wasn't perfect — C headers worked but didn't use classes or templates. But existing code was immediately useful, and migration happened incrementally.
Den's namespace system is the bridge entry point. Namespaces are isolated aspect containers that import external definitions without collision:
# User's flake.nix — imports Clan services via bridge
imports = [
(den.namespace "clan" [ inputs.clan-bridge ])
];
# Clan services available as den aspects
den.aspects.web1 = {
includes = [
clan.borgbackup # ← bridged Clan service
clan.zerotier # ← bridged Clan service
];
nixos.services.nginx.enable = true; # ← native den aspect
terranix.resource.hcloud_server = {}; # ← terraform (Clan can't do this)
};The bridge translates _class = "clan.service" into den namespace aspects:
| Clan construct | Den translation |
|---|---|
roles.<r> |
Nested aspect service/<r> with provides |
roles.<r>.interface |
aspect.settings via gen-schema |
roles.<r>.perInstance |
Parametric aspect (__args) |
perMachine |
Static aspect (class content) |
manifest |
aspect.meta |
exports |
Pipe declarations |
constraints |
gen-derive rules |
roles.<r>.machines |
gen-select members |
-
Multi-target output — a bridged borgbackup service could gain a
terranixkey for provisioning backup storage, adevshellkey for operator tooling, and atestskey for nix-unit validation. In Clan these would be separate, uncoordinated integrations. -
Scope graph evaluation — building one backup client doesn't force evaluation of all servers. The cross-role references that cause Clan's O(R×I×M) blowup become lazy import edges.
-
den-diagram visualization — bridged services participate in fleet topology rendering. Clan has no equivalent.
-
Policy-driven composition — aspects compose based on scope context, not explicit machine lists. Adding a backup server means tagging a host, not editing a service definition.
-
Dedup and classification — den's pipeline handles diamond includes, duplicate aspect resolution, and class key classification. Clan services that produce the same NixOS option from multiple paths get silent merge conflicts.
Tier 1 (Day 1): Single-role services with no cross-role references. Direct translation: perInstance.nixosModule → den class key. ~20 of 32 Clan services.
Tier 2 (Week 1): Multi-role services with independent roles. Each role → nested aspect. hello-world, services with orthogonal roles.
Tier 3 (Week 2-3): Multi-role services with bidirectional references (borgbackup server↔client). Cross-role roles.*.machines → scope graph import edges + lazy peers.* queries.
Tier 4 (Month 1+): Services using vars/generators, exports, and constraints. Requires gen-vars and gen-derive constraint rules.
Services don't migrate all at once. A bridged Clan service, a partially-migrated service, and a fully native den service all coexist via namespace isolation:
den.aspects.web1 = {
includes = [
clan.matrix-synapse # bridge: unchanged Clan service
clan.borgbackup # bridge: multi-role, scope graph queries
];
nixos.services.nginx.enable = true; # native: den aspect
terranix.resource.hcloud_server = {} # native: can't do this in Clan
devshell = { pkgs, ... }: { # native: operator shell
commands = [{ package = pkgs.opentofu; }];
};
};The native aspects add capabilities (terraform, devshells, tests) that the bridged services can gradually adopt.
Gap: No secrets infrastructure.
Design:
den.vars.generators.wireguard-key = {
scope = "host"; # per-host node in scope graph
files.private.secret = true;
files.public.secret = false;
dependencies = [ "ssh-host-key" ]; # I-edge in scope graph
script = ''
wg genkey > $out/private
wg pubkey < $out/private > $out/public
'';
};
# Backend selection via gen-derive rules
den.vars.backends.sops = { /* ... */ };
den.vars.policies.production-uses-sops = { host, ... }:
lib.optional (builtins.elem "production" (host.tags or []))
(policy.varBackend "sops");
# In aspects — resolved via scope graph
den.aspects.wireguard.nixos = { vars, ... }: {
networking.wireguard.interfaces.wg0.privateKeyFile =
vars.wireguard-key.files.private.path;
};Scope graph integration: generators become nodes, dependencies become I-edges, gen-graph topoSort sequences generation, fleet-level vars inherit via P-edges.
Why den's approach is better than Clan's: Vars participate in the scope graph — visibility follows the entity hierarchy automatically. A host-level var is visible to its users. A fleet-level var is visible to all hosts. Clan's vars use flat path conventions (vars/per-machine/...) with no structural visibility model.
Size: ~300-400 lines core + ~100 per backend.
Gap: No service-first thinking.
Design:
den.services.borgbackup = {
meta = {
description = "Encrypted backup with borg";
categories = [ "Backup" ];
};
roles.server = {
members = sel.attrs { tag = "backup-server"; };
settings = { lib, ... }: {
options.directory = lib.mkOption { default = "/var/lib/borgbackup"; };
};
aspect = { settings, peers, vars, ... }: {
nixos = {
services.borgbackup.repos = lib.genAttrs peers.client (name: {
path = "${settings.directory}/${name}";
});
};
# The same service can also emit terraform and devshell config
terranix.resource.hcloud_volume.backup.size = 100;
devshell = { pkgs, ... }: {
commands = [{ name = "borg-status"; command = "..."; }];
};
};
};
roles.client = {
members = sel.not (sel.attrs { tag = "no-backup"; });
settings = { lib, ... }: {
options.startAt = lib.mkOption { default = "*-*-* 01:00:00"; };
};
aspect = { settings, peers, ... }: {
nixos.services.borgbackup.jobs = lib.genAttrs peers.server (name: {
repo = "borg@${name}:."; startAt = settings.startAt;
});
};
};
};den.services is sugar — it compiles down to aspects + policies + pipes. The FX pipeline / HOAG evaluator only sees aspects. peers is a lazy gen-scope query, not an eager lib.mapAttrs over all machines.
Why den's approach is better than Clan's: Multi-target output (the same service definition produces NixOS config, terraform resources, and devshell tooling). Selector-based membership (not machine lists). O(subtree) evaluation (not O(R×I×M)).
Size: ~200-300 lines for service→aspect translation + ~100 for peer resolution.
Gap: Aspects are opaque.
Design:
den.aspects.nginx = {
meta = {
description = "Nginx reverse proxy with ACME";
categories = [ "Web" "Network" ];
requires = [ "acme" ];
provides = [ "http-proxy" ];
};
settings = { lib, ... }: {
options.port = lib.mkOption { type = lib.types.port; default = 443; };
};
nixos = { settings, ... }: { /* ... */ };
};
# Queryable
den.lib.aspects.catalog # → { nginx = { description, categories, schema }; }
den.lib.aspects.byCategory "Network" # → [ "nginx" "wireguard" ]
den.lib.aspects.validateDependencies # → [ { aspect = "nginx"; missing = "acme"; } ]gen-schema introspection generates the schema. gen-derive validates requires/provides. External tools consume catalog as JSON.
Size: ~150 lines.
Gap: No fleet-level grouping.
Design:
den.fleet = {
meta.name = "production";
meta.domain = "example.com";
tags = {
production = [ "web1" "web2" "db1" ];
gpu = [ "ml1" "ml2" ];
backup-server = [ "storage1" ];
# Computed tags
nixos = hosts: builtins.filter (h: hosts.${h}.system == "x86_64-linux") (builtins.attrNames hosts);
};
environments.prod = {
hosts = sel.attrs { tag = "production"; };
meta.region = "us-east";
};
};Tags become scope graph node attributes. Environments add a scope level between fleet and host. gen-select queries replace flat set expansion.
Size: ~100-150 lines.
Gap: Implicit settings layering.
Design:
# Layer 1: Aspect declares interface
den.aspects.nginx.settings = { lib, ... }: {
options.port = lib.mkOption { type = lib.types.port; default = 80; };
};
# Layer 2: Policy-driven defaults
den.policies.nginx-production = { host, ... }:
lib.optional (builtins.elem "production" (host.tags or []))
(policy.configure "nginx" { port = 443; });
# Layer 3: Host-level override
den.hosts.x86_64-linux.web1.aspects.nginx.settings.port = 8443;Three explicit layers via gen-schema mixin composition (Bracha 1990). den-diagram can visualize which layer provided which value.
Size: ~100 lines.
| Priority | Work item | Depends on | Size | Value |
|---|---|---|---|---|
| 1 | Spike 5: Settings stratification | — | ~100 lines | Cleanest, proves the pattern |
| 2 | Spike 4: Fleet inventory + tags | — | ~100-150 lines | Enables everything else |
| 3 | Spike 3: Aspect manifests | — | ~150 lines | Catalog, validation, external tooling |
| 4 | Spike 1: gen-vars | Spike 4 | ~400-500 lines | Unblocks secrets, Tier 4 bridge |
| 5 | Bridge Tier 1-2 | Spikes 3-5 | ~200 lines | Immediate 20-service library |
| 6 | Spike 2: Service model | Spikes 1, 4 | ~300-400 lines | Native distributed services |
| 7 | Bridge Tier 3-4 | Spikes 1-2 | ~300 lines | Full 32-service library |
Total: ~1,350-1,800 lines for all spikes + bridge.
| Clan feature | Why not |
|---|---|
| Evaluation model (O(R×I×M) evalModules) | Den's scope graph is strictly better — O(subtree) |
| JSON Schema generation (26K lines) | gen-schema introspection replaces this |
| NixOS-coupled pipeline | Den processes outside evalModules — this is the performance win |
| Tag resolution (flat set expansion) | gen-select is strictly more expressive |
| String-based scopes | Scope graphs with formal resolution are the whole point |
| Module merge for composition | Algebraic graph composition (Mokhov) is provably order-independent |
| Two-target limitation (nixos/darwin) | Den's class system handles any number of output targets |
- Not forking Clan. The bridge consumes Clan services unchanged.
- Not matching Clan's API. Bridged services use den's API, not Clan's. The translation is structural.
- Not replacing Clan's CLI. Den's tooling story is separate.
- Not all-or-nothing. Each spike ships independently. The bridge is useful at Tier 1 without waiting for Tier 4.
- Not borrowing implementation patterns. Den borrows features (secrets, service roles, manifests, tags) — not how Clan implements them. The implementations use den's scope graph, gen libraries, and class system.