multi-host Nix configuration for nix-darwin, NixOS, and Home Manager. hosts select capabilities explicitly; each capability keeps its system, user, package, scripts, tests, and assets together.
across every host and feature, the repo manages the live system without
exclusively controlling it. programs read and write editable, tracked files:
pnpm add -g updates the global manifest and installs locally; Pi can modify its
own modules; Niri consumes live config edits.
Nix supplies prerequisites, links, and services. native install/build/reload workflows remain available; rebuild/switch is required only where the change needs system activation.
the goal is prompt, automatic convergence of published changes across hosts, with manual catch-up always available—not a claim of complete implementation. preserve unpublished edits rather than resetting them during updates.
installation and runtime must use the same live source and manifest/lockfile. replacing live files with immutable snapshots or deployment-only copies requires explicit agreement.
flowchart TD
F[flake.nix<br/>inputs and output wiring] --> H[hosts/name/default.nix]
H -->|explicit imports| M[modules/feature]
M --> D[default.nix<br/>system entry point]
M --> HM[home.nix<br/>Home Manager fragment]
M --> P[package.nix<br/>derivation]
M --> L[lib.nix<br/>pure helpers]
M --> T[tailnet-app.nix<br/>service declaration]
T --> G[generated tailnet artifacts]
flake.nixowns inputs, platform constructors, overlays, checks, packages, and named configuration outputs.hosts/<name>/owns machine identity, hardware and topology, feature selection, and host-specific values.modules/<feature>/owns a reusable capability across system and Home Manager scopes. supporting scripts, tests, packages, and assets stay with that feature.- hosts import features directly. repeated imports are intentional: there are
no implicit
base,desktop, ordevbundles. assets/,config/,overlays/, andscripts/contain repository-wide support files rather than Nix architecture roots.
| file | contract |
|---|---|
default.nix |
system-level feature entry point; importing modules/foo selects it |
home.nix |
direct Home Manager module, imported by the feature's system adapter |
package.nix |
reusable package expression, imported directly rather than placed in a module imports list |
lib.nix |
pure data or helpers without a NixOS option graph |
service.nix |
explicit deployment integration when packaging and service policy are separate |
tailnet-app.nix |
declarative tailnet and Cloudflare metadata discovered by the catalog |
credential.nix |
the feature's stable credential owner; consumers import it and set its requirement flag |
secrets.yaml |
canonical ciphertext adjacent to its credential.nix owner |
| output | platform | role |
|---|---|---|
mbp-m2 |
aarch64-darwin | primary graphical workstation |
mbp-m5 |
aarch64-darwin | work-issued development workstation |
mmn-m4 |
aarch64-darwin | household storage and media service host |
lgo-z2e |
x86_64-linux | Niri/Jovian graphical system |
htz-relay |
x86_64-linux | storage, Syncthing, and application relay |
gru-relay |
x86_64-linux | Tailscale exit node and ingress relay |
# inspect inputs and format
nix flake metadata
nix fmt
# verify the Darwin host before activation
nix build .#darwinConfigurations.mbp-m2.system --dry-run
nix build .#darwinConfigurations.mbp-m2.system
sudo darwin-rebuild switch --flake .#mbp-m2
# verify a NixOS host before activation
nix build .#nixosConfigurations.lgo-z2e.config.system.build.toplevel --dry-run
nix build .#nixosConfigurations.lgo-z2e.config.system.build.toplevel
sudo nixos-rebuild switch --flake .#lgo-z2ereplace the host name with the target configuration. do not cross-build by default.
all hosts import the hourly upgrade policy through modules/nix. Linux uses
system.autoUpgrade without automatic reboots; Darwin uses a root launchd job.
Darwin can opt into build-only with system.autoUpgrade.operation = "build".
these jobs consume the published GitHub flake, not an editable local checkout.
they do not wait for idle or supply health-based rollback.
system upgrade jobs alone do not synchronize live checkouts, install their dependencies, or reload programs.
flake.lock and custom package pins still need repository updates. the
flake-update workflow proposes those changes; host timers do not advance them.
features publish services with modules/**/tailnet-app.nix. the catalog
validates those declarations and projects them into:
generated/fleet-apps.jsoncloudflare/apps.auto.tfvars.jsontailscale/services.jsontailscale/capabilities.json
regenerate and verify the projections after changing a declaration:
nix run .#generate-tailnet-artifacts
nix build .#checks.aarch64-darwin.tailnet-artifactsgenerated JSON is output, not an additional source of truth.
secrets use sops-nix. .sops.yaml contains public recipients, while encrypted
values and runtime declarations live beside the domain that owns them. consumer
count does not affect ownership: a second consumer imports the same
credential.nix rather than moving or copying its ciphertext.
sops modules/<feature>/secrets.yaml
sops updatekeys modules/<feature>/secrets.yaml
nix build .#checks.aarch64-darwin.credential-ownership
nix build .#checks.aarch64-darwin.credential-host-closuresmodules/secrets/default.nix owns only shared sops/age infrastructure.
never commit private age keys. see SECRETS.md for setup and
rotation.