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"
];
};
}
];
};
}