A NixOS configuration using the Dendritic Pattern - an organic configuration growth pattern with automatic module discovery.
Based on the golden standard from mightyiam/infra.
Nix files (they're all flake-parts modules) are automatically imported. Nix files prefixed with an underscore are ignored. No literal path imports are used. This means files can be moved around and nested in directories freely.
Note
This pattern has been the inspiration of an auto-imports library, import-tree.
This flake exposes two mergeable aggregators:
flake.nixosModules
: NixOS modules (freeform, nested namespaces allowed)flake.homeManagerModules
: Home Manager modules (freeform; withbase
,gui
, and per-app underapps
)
Modules register themselves under these namespaces (e.g., flake.nixosModules.workstation
, flake.homeManagerModules.base
).
Composition uses named references, for example:
{ config, ... }:
{
configurations.nixos.myhost.module = {
imports = with config.flake.nixosModules; [ base workstation ];
};
}
Use lib.hasAttrByPath
+ lib.getAttrFromPath
when selecting optional modules to avoid ordering issues.
- Roles are assembled from per-app modules under
flake.nixosModules.apps
, usingconfig.flake.lib.nixos.getApps
/getApp
for lookups. - Avoid lexical
with
overconfig.flake.nixosModules.apps
; the helper namespace keeps evaluation pure and consistent. - Import roles via
flake.nixosModules.roles.<name>
(for example,.dev
,.media
,.net
).
Example host composition using the role namespace:
{ config, ... }:
{
configurations.nixos.system76.module = {
imports =
(with config.flake.nixosModules; [
workstation
])
++ [
config.flake.nixosModules.roles.dev
];
};
}
For a complete, type-correct composition plan and guidance, see
docs/configuration-architecture.md
.
Enter the development shell:
nix develop
Useful commands:
nix fmt
– format filespre-commit run --all-files
– run all hooks
The build.sh
helper refuses to run if the git worktree is dirty (tracked changes, staged changes, or untracked files) to keep builds reproducible. Override with --allow-dirty
or ALLOW_DIRTY=1
only when you know what you’re doing.
- Encrypt the payload – run
sops secrets/<name>.yaml
(orsops -e -i …
) so the file is stored as ciphertext. The canonical.sops.yaml
in this repo already targets everything undersecrets/
. - Declare the secret in Nix – add an entry under
sops.secrets."<namespace>/<name>"
(system or Home Manager). PointsopsFile
to the encrypted file, setkey
when selecting a single YAML attribute, and write the decrypted material to a runtime path using%r
. - Consume via the module API – reference
config.sops.secrets."<namespace>/<name>".path
(orplaceholder
) from services, wrappers, or templates. Never read secrets at evaluation time.
Example (Context7 MCP key for Codex):
sops.secrets."context7/api-key" = {
sopsFile = ./../../secrets/context7.yaml;
key = "context7_api_key";
path = "%r/context7/api-key";
mode = "0400";
};
The Codex module wraps the decrypted path in a small script and only enables the MCP server when the secret exists, keeping evaluation pure while allowing runtime access.
The following files in this repository are generated and checked using the files flake-parts module:
.actrc
.gitignore
.sops.yaml
README.md
Some explicit flake inputs exist solely for the purpose of deduplication.
They are the target of at least one <input>.inputs.<input>.follows
.
But what if in the future all of those targeting follows
are removed?
Ideally, Nix would detect that and warn.
Until that feature is available those inputs are prefixed with dedupe_
and placed in an additional separate inputs
attribute literal
for easy identification.
This at the top level of the flake.nix
file:
nixConfig.abort-on-warn = true;
Note
It does not currently catch all warnings Nix can produce, but perhaps only evaluation warnings.