Skip to main content

NixOS: The Dendritic Pattern

NixOS - This article is part of a series.
Part 3: This Article

Introduction
#

The Dendritic Pattern is a simple, yet powerful approach to defining complex Nix projects containing a mix of nixos and home-manager modules without the need for spaghetti code or complex wiring functions. This is accomplished using the Flake Parts module to define every aspect as a top level function, all of which may be merged at evaluation time. Each module can be imported at the flake level using the handy Import-Tree module.

Defining a dendritic flake
#

The flake.nix of a dendritic nixos configuration should contain only three elements:

  • A description of the flake
  • A list of imports for the flake
  • A single output making use of the flake parts mkFlake function

A simple flake to create a nixos-configuration with modules defined in the modules subdirectory looks like:

{
  description = "Robbie's NixOS flake";

  inputs = {
    nixpkgs = {
      url = "github:NixOS/nixpkgs/nixos-25.11";
    };
  };

  outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
}

Defining a module for basic settings
#

A single module can be defined for re-used nix and home-manager settings to reduce code duplication across configurations:

{
  self,
  inputs,
  ...
}:
{
  flake.modules.nixos.settings =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      imports = [
        inputs.home-manager.nixosModules.home-manager
        inputs.impermanence.nixosModules.impermanence
        inputs.sops-nix.nixosModules.sops
        inputs.disko.nixosModules.disko
        inputs.stylix.nixosModules.stylix
      ];

      nix.settings = {
        auto-optimise-store = true;
        experimental-features = [
          "nix-command"
          "flakes"
        ];
      };

      nixpkgs = {
        config.allowUnfree = true;
        overlays = [
          self.overlays.unstable-packages
          self.overlays.additional-packages
        ];
      };

      home-manager = {
        useGlobalPkgs = true;
        useUserPackages = true;
        backupFileExtension = "backup";
        sharedModules = [
          {
            imports = [
              inputs.sops-nix.homeManagerModules.sops
              inputs.stylix.homeModules.stylix
            ];
            home.stateVersion = "25.11";
          }
        ];
      };

      stylix.homeManagerIntegration.autoImport = false;
      users.mutableUsers = false;
      system.stateVersion = "25.11";
    };
}

Defining a feature module
#

Though modules are composable, each module file should handle a single repsonsibility. For example, a desktop module may contain many individual feature modules to cover the desktop environments, audio interfaces, etc…\

Single feature
#

An “audio” module may look like:

{
  inputs,
  ...
}:
{
  flake.modules.nixos.audio =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      options = {
        audio.enable = lib.mkEnableOption "audio using pipewire";
      };

      config = lib.mkIf config.audio.enable {
        services.pipewire = {
          enable = true;
          pulse.enable = lib.mkDefault true;
        };
      };
    };
}

Multi-feature
#

This may be imported into a high-level “desktop” module like:

{
  inputs,
  ...
}:
{
  flake.modules.nixos.desktop =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      imports = [
        inputs.self.modules.nixos.audio
        inputs.self.modules.nixos.bluetooth
        inputs.self.modules.nixos.cosmic-desktop
        inputs.self.modules.nixos.kde-plasma
        inputs.self.modules.nixos.kde-connect
        inputs.self.modules.nixos.printing
        inputs.self.modules.nixos.scanning
        inputs.self.modules.nixos.steam
        inputs.self.modules.nixos.virtualisation
        inputs.self.modules.nixos.qmk
      ];

      options = {
        desktopEnvironment = lib.mkOption {
          type = lib.types.enum [
            "plasma"
            "cosmic"
          ];
          default = "plasma";
          description = "Select desktop environment: Plasma or COSMIC.";
        };
      };

      config = {
        services.flatpak.enable = true;
        bootloader.pretty = lib.mkDefault true;
        audio.enable = lib.mkDefault true;
        bluetooth.enable = lib.mkDefault true;
        cosmic-desktop.enable = if config.desktopEnvironment == "cosmic" then true else false;
        kde-plasma.enable = if config.desktopEnvironment == "plasma" then true else false;
        kde-connect.enable = lib.mkDefault true;
        printing.enable = lib.mkDefault true;
        scanning.enable = lib.mkDefault true;
        steam.enable = lib.mkDefault true;
        virtualisation.enable = lib.mkDefault true;
        qmk.enable = lib.mkDefault true;

        home-manager.sharedModules = [
          {
            imports = [
              inputs.nix-flatpak.homeManagerModules.nix-flatpak
              inputs.plasma-manager.homeModules.plasma-manager
              inputs.cosmic-manager.homeManagerModules.cosmic-manager
            ];
          }
        ];

        assertions = [
          {
            assertion = !(config.cosmic-desktop.enable && config.kde-plasma.enable);
            message = "Cannot enable both COSMIC and Plasma at the same time.";
          }
        ];
      };
    };
}

The factory method
#

Adding a named “factory” flake module with an unspecified attribute list as its type will allow for the creation of factory modules. These modules look similar to typical nixos or home-manager modules with the exception that they take an addtional attribute set as the initial argument and can then be re-used to instantiate multiple modules using the same logic.

The factory module should look like:

{
  lib,
  ...
}:
{
  options.flake.factory = lib.mkOption {
    type = lib.types.attrsOf lib.types.unspecified;
    default = { };
  };
}

This flake module can then be used to instantiate a desktop-user nixos module like:

{
  self,
  inputs,
  ...
}:
{
  config.flake.factory.desktop-user =
    {
      username,
      isAdmin,
    }:
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      config = lib.mkMerge [
        {
          users.users."${username}" = {
            initialPassword = lib.mkDefault "password";
            isNormalUser = true;
            home = "/home/${username}";
            extraGroups = [
              "networkmanager"
              "docker"
              "libvirtd"
            ]
            ++ lib.optionals isAdmin [
              "wheel"
            ];
          };

          home-manager.users."${username}" = {
            imports = [
              inputs.self.modules.homeManager.development
              inputs.self.modules.homeManager.utilities
              inputs.self.modules.homeManager.web
              inputs.self.modules.homeManager.gaming
              inputs.self.modules.homeManager.editing
              inputs.self.modules.homeManager.backup
            ];
          };
        }

        (lib.mkIf (config.secrets.enable && config.secrets.passwords.enable) {
          sops.secrets."passwords/${username}".neededForUsers = true;
          users.users.${username} = {
            initialPassword = null;
            hashedPasswordFile = config.sops.secrets."passwords/${username}".path;
          };
        })

        (lib.mkIf config.impermanence.enable {
          environment.persistence."/persist".users.${username} = {
            directories = [
              "Desktop"
              "Documents"
              "Downloads"
              "Music"
              "Pictures"
              "Videos"
              "Games"
              "Books"
              "nix-config"
              {
                directory = ".ssh";
                mode = "0700";
              }
              {
                directory = ".local/share/keyrings";
                mode = "0700";
              }
              {
                directory = ".local/share/kwalletd";
                mode = "0700";
              }
              ".local/share/flatpak"
              ".local/share/Steam"
              ".local/share/PrismLauncher"
              ".local/state/cosmic"
              ".local/state/cosmic-comp"
              ".config"
              ".var"
              ".vscode-oss/extensions"
            ];
            files = [
              ".bash_history"
              ".zsh_history"
            ];
          };
        })
      ];
    };
}

Creating a system configuration
#

Bringing everything together, we can create a complete NixOS system configuration using the flake.nixosConfigurations.<hostname> function. A laptop system module may look like:

{
  self,
  inputs,
  lib,
  ...
}:
{
  flake.modules.nixos.robbie-laptop = lib.mkMerge [
    (self.factory.desktop-user {
      username = "robbie";
      isAdmin = true;
    })
    {
      home-manager.users.robbie = {
        programs.git = {
          enable = true;
          settings.user = {
            name = "robbiejennings";
            email = "[email protected]";
          };
        };
        theme = {
          image = {
            url = "https://raw.githubusercontent.com/AngelJumbo/gruvbox-wallpapers/refs/heads/main/wallpapers/photography/forest-2.jpg";
            hash = "sha256-RqzCCnn4b5kU7EYgaPF19Gr9I5cZrkEdsTu+wGaaMFI=";
          };
          base16Scheme = "gruvbox-material-dark-hard";
        };
        secrets = {
          enable = true;
          vuescan.enable = true;
          rclone.enable = true;
          restic.enable = true;
        };
      };
    }
  ];

  flake.nixosConfigurations.xps15 = inputs.nixpkgs.lib.nixosSystem {
    modules = [
      inputs.self.modules.nixos.settings
      inputs.self.modules.nixos.xps15
      inputs.self.modules.nixos.core
      inputs.self.modules.nixos.desktop
      inputs.self.modules.nixos.robbie-laptop
      {
        networking.hostName = "xps15";
        desktopEnvironment = "cosmic";
        secrets = {
          enable = true;
          passwords.enable = true;
        };
        impermanence.enable = true;
        environment.persistence."/persist" = {
          hideMounts = true;
          directories = [
            "/var/log"
            "/var/lib/bluetooth"
            "/var/lib/nixos"
            "/var/lib/systemd/coredump"
            "/var/lib/libvirt"
            "/var/lib/netbird"
            "/etc/NetworkManager/system-connections"
            "/etc/nixos"
            "/root/.ssh"
          ];
        };
      }
    ];
  };
}

NixOS - This article is part of a series.
Part 3: This Article