Architecture
This page is the map of the repository: what lives where, how a host gets built, and where to start when adding something new.
Directory layout
| Path | Purpose |
|---|---|
flake.nix | Inputs and output wiring. Host/user discovery is delegated to systems/ and homes/. |
systems/<arch>/<host>/ | One directory per host, auto-discovered. The directory name is the hostname. |
systems/iso/ | Installer ISO definition and the list of hosts that get an iso-* image. |
modules/nixos/ | NixOS modules (desktop, services, system, security, users, …). |
modules/darwin/ | nix-darwin modules (homebrew, services, system, users). |
modules/home-manager/ | Home Manager modules — the largest tree, see background vs foreground. |
modules/devix/ | Development-environment mechanism: a registry of languages, addons, LSPs, and formatters consumed by editors (helix, zed, opencode). Exposed as homeModules.devix and devenvModules.*. See devix. |
homes/<user>/ | Per-user Home Manager entry points, auto-discovered for standalone homeConfigurations. SSH public keys live in homes/<user>/keys/. |
lib/ | Small helper library layered onto nixpkgs.lib (directory listing, tag predicate, eachSystem). |
overlays/ | Nixpkgs overlays: custom packages, modifications, pkgs.unstable, … |
packages/ | Custom packages: top-level/ (auto-discovered), custom/ (namespaced under pkgs.custom), plus python/kodi/home-assistant scopes and the docs book. |
shells/ | Dev shells (nix develop .#go, .#website, .#nix). |
secrets/ | sops-encrypted secrets. Recipients (age keys per user and host) are declared in secrets/.sops.yaml. |
scripts/ | Repo tooling (package bump script, CI checks) — driven via the justfile. |
docs/ | This mdBook. Published via GitHub Pages, buildable with nix build .#docs. |
justfile | Task runner: just --list shows all daily commands. |
How a host is built
flake.nix
└─ systems/default.nix scans systems/<arch>/ for host directories
├─ arch contains "darwin"? ─ nix-darwin's darwinSystem
│ └─ modules/darwin + home-manager + stylix + sops + homes/nixos.nix
└─ otherwise ─────────────── lib.nixosSystem
└─ modules/nixos + home-manager + facter + homes/nixos.nix
└─ homes/nixos.nix instantiates home-manager for
homes.users (default: the primary user), each importing
homes/<user>/ + modules/home-manager
networking.hostNameis forced to the directory name.- A
facter.jsonin the host directory is picked up automatically (hardware detection via nixos-facter). - A
nixpkgs.nixin the host directory supplies per-host nixpkgsconfig, merged over the shared one. This is the only way to set it:pkgsis instantiated insystems/default.nixoutside the module system, so a host cannot usenixpkgs.configin itsconfiguration.nix(the nixpkgs module rejects it). Seesystems/x86_64-linux/kyuubi/nixpkgs.nix, which permits an insecure Broadcom wifi driver for that host alone. users.primaryUsermust be set in every host config; the user account itself comes frommodules/nixos/users/(ormodules/darwin/users/).- Standalone
homeConfigurations.<user>(for non-NixOS machines) are generated from thehomes/<user>/directories, independent of any host.
The tag system
Hosts describe themselves with a list of tags instead of toggling dozens of options:
# systems/x86_64-linux/madara/configuration.nix
system.tags = [ "desktop" "workstation" "development" "management" ];
- The known tags are enumerated in
modules/shared/tags.nix— the option type rejects anything else at eval time, so a typo or a tag without consumers cannot slip in silently. Current tags:desktop,laptop,workstation,development,management,nas,gaming,server(each with a one-line meaning in that file). - Modules query tags through
config.lib.tags.hasTag "<tag>"(the predicate lives inlib/has-tag.nix; the option is declared undermodules/{nixos,darwin}/system/tags/). - Tag modules under
modules/{nixos,darwin}/system/tags/translate tags into concrete settings (e.g.desktopenables the desktop environment stack,managementinstalls colmena,gamingenables Steam). Server roles key on theservertag — tailscale exit-node/connector/SSH advertising and the no-sound-stack default — never on the absence of an interactive tag: a workstation or laptop withoutdesktopis still somebody’s machine. - Colmena reuses the same tags for deployment targeting:
colmena apply --on @desktop(wired viacolmena.deployment.tags). - Home Manager cannot read
config.lib.tagswithout infinite recursion, somodules/home-manager/imports.niximports the predicate directly againstosConfigand maps tags onto its ownhosts.{desktop,laptop,workstation,development}.enableoptions. Inside home-manager modules, gate onconfig.hosts.*.enable.
Background vs foreground
modules/home-manager/ is split by session type, not by program category:
background/— everything that works on a headless machine or over SSH: shell, git, editors, CLI tools, AI tooling, user services. Imported unconditionally.foreground/— everything that needs a display: desktop environments, bars, GUI programs, fonts-for-GUI. Gated behindforeground.enable, which defaults from thedesktoptag (darwin), thegui.enableoption (NixOS), ortrue(standalone home-manager).theming/— stylix wiring, wallpaper, color scheme; enabled with thedesktoptag.development/— policy: which languages/tools frommodules/devix(the mechanism) are turned on fordevelopment-tagged hosts.
devix
Development-environment mechanism: describe a language once — its language servers, formatters and indentation — and every editor that consumes the description configures itself from it. Stylix’s idea, applied to dev tooling.
It is documented in its own section, starting at devix: concepts, usage, adding a language, adding an editor, and a generated support matrix and option reference.
The one thing worth repeating here is the split this repository depends on:
modules/devix is pure mechanism and enables nothing, while
modules/home-manager/development/ is the policy that decides which languages
are on for which hosts.
Adding a host
- Create
systems/<arch>/<hostname>/default.nix(plusconfiguration.nix,hardware-configuration.nixorfacter.json, and optionallydisk.nixfor disko). The directory is discovered automatically. - Set
users.primaryUserandsystem.tagsin the config. - Add the host’s age key to
secrets/.sops.yamland re-encrypt:just secrets-rekey(key comes from the host’s SSH key, see sops). - For colmena deploys, set
colmena.deployment.targetHost. - If the host should get an installer image, add it to the list in
systems/iso/default.nix— thenjust iso <hostname>.
Adding a user
- Create
homes/<user>/default.nix(auto-discovered ashomeConfigurations.<user>); put SSH public keys inhomes/<user>/keys/. - Create
modules/nixos/users/<user>.nix(auto-imported; seemirza.nixfor the pattern). - Set
users.primaryUser = "<user>"on the hosts that belong to them, and add their age key tosecrets/.sops.yaml.
Custom options cheat sheet
Options defined by this flake (as opposed to upstream NixOS/HM options):
| Option | Defined in | Meaning |
|---|---|---|
system.tags | modules/{nixos,darwin}/system/tags/ | Host capability tags (see above) |
users.primaryUser | modules/{nixos,darwin}/users/ | The one human this machine belongs to |
users.users.<u>.isAdmin | modules/nixos/users/ | wheel + virtualisation groups |
homes.users / homes.enable | homes/nixos.nix | Which users get home-manager |
gui.enable | modules/nixos/programs/ | “This host has GUI applications” (feeds foreground.enable) |
desktop-environment.enable | modules/nixos/desktop/ | Desktop environment stack |
hosts.{desktop,laptop,workstation,development}.enable | modules/home-manager/imports.nix | HM-side mirror of the tags |
devix.* | modules/devix/ | Development environments — see the devix section |
foreground.enable | modules/home-manager/foreground/ | GUI-facing home config |
theming.{enable,scheme,image} | modules/home-manager/theming/, modules/nixos/system/theming.nix | Stylix scheme/wallpaper |
rssh.enable, yubikey.enable, cachix.enable, latex.enable | modules/nixos/** | Feature toggles for individual services |
NAS behaviour follows the nas tag, and the TPM2 stack is driven by the
upstream security.tpm2.enable (this flake just layers PKCS11/tooling on top).
New custom options should be namespaced (or documented here) so they stay distinguishable from upstream options.