How do you structure your NixOS configs?

There are different “Dendritic” implementations (at least four that I know of) because the Dendritic pattern is not about having a rigid directory/file structure that mandates where to place your files - unlike other configuration libs/frameworks. That’s because Dendritic is more a pattern - the dendritic repo does not even have nix code, it just tries to explain things and maybe it not still that clear, I’ll try to expand here and you tell me if this helps we can add it to the documentation-.

And the pattern is this: Each ./modules/**/*.nix file is a flake-parts module and each file can contribute to different nix module classes at the same time by using flake-parts-modules , eg:

# modules/terminal.nix -- the important thing here is flake-parts' modules:
{
  flake.modules.nixos.myhost = {pkgs, ...}: {
     environment.systemPackages = [ pkgs.ghostty ];
  }; 

  flake.modules.darwin.myhost = {pkgs, ...}: {
     environment.systemPackages = [ pkgs.iterm2 ];
  };
}

The important thing here is, instead of having top-level files be an nixos-module or an nix-darwin-module or an home-manager-module, etc. you have a single feature flake-parts module like the one shown above that itself adds configurations to your flake modules.

# modules/vic.nix -- configures os-level vic user
let
  userName = "vic"; # a binding just to show another dendritic advantage: 
  # you use let bindings directly across different module classes, instead of having
  # to polute nixos config specialArgs to propagate values accross module classes.
in
{
  flake.modules.nixos.myhost = {
     users.users.${userName} = { isNormalUser = true; extraGroups = [ "wheel" ]; };
  };

  flake.modules.darwin.myhost = {
     system.primaryUser = userName;
  };

  flake.modules.homeManager.${userName} =
    { pkgs, lib, ... }:
    {
      home.username = lib.mkDefault userName;
      home.homeDirectory = lib.mkDefault (if pkgs.stdenvNoCC.isDarwin then "/Users/${userName}" else "/home/${userName}");
      home.stateVersion = lib.mkDefault "25.05";
    };
}

Notice how configuring the os-level user is different if you are on nixos or nix-darwin, but this single vic.nix file is a cross-cutting concern (the concern of configuring the vic user) that contributes to both and also to an home-manager module. You will also have other files (eg, modules/vic/secrets.nix) that also contributes to flake.modules.homeManager.vic but it appends to that module another concern!

That’s the basic idea behind the pattern. How you organize your files is free to you, but since each file is mostly safe contained you can move it around and refactor easily large systems configurations. File location does not matter since they are all loaded by import-tree (or similar).

So each file contributes to some flake.modules.<class>.<name> of your liking, how you split and organize modules is up to you.

Then you finally import those flake.modules.<class>.<name> on their respective nixos/darwin configurations (how you wire things up is not defined by the pattern), I for example have a osConfigurations.nix that is itself a flake-module, and it exposes my flake hosts and loads root modules on them.

1 Like