Skip to main content

Overview

The homelab uses a modular architecture to organize configurations. Modules are reusable configuration components that can be enabled or disabled with simple options.

Module Types

There are three types of modules corresponding to the three configuration types:

NixOS Modules

modules/nixos/System-level configuration for NixOS systems

Home Manager Modules

modules/home/User-level configuration and applications

Droid Modules

modules/droid/Android device configuration via Nix-on-Droid

Module Structure

All modules are automatically discovered and imported using a discovery mechanism:
modules/nixos/default.nix
{lib, ...}: let
  modules = lib.discover ./.;
in
  modules
  // {
    default = {
      imports = builtins.attrValues modules;
    };
  }
This means any .nix file you add to a module directory is automatically available in your configurations.

NixOS Modules

Located in modules/nixos/, these modules configure system-level settings.

Module Categories

Essential system configuration:
modules/nixos/core/
├── boot.nix              # Bootloader configuration
├── clamav.nix            # Antivirus
├── gitops.nix            # Automatic updates
├── nixconf.nix           # Nix daemon configuration
├── secrets.nix           # SOPS secrets management
├── users.nix             # User management
├── hardware/
│   ├── default.nix       # Hardware detection
│   ├── adb.nix          # Android Debug Bridge
│   ├── bluetooth.nix    # Bluetooth support
│   ├── gpu/             # GPU drivers
│   └── hid/             # Input devices
└── networking/
    ├── network-manager.nix
    ├── openssh.nix
    ├── tailscale.nix
    └── ...
Usage:
core = {
  boot.enable = true;
  hardware.enable = true;
  networking.tailscale.enable = true;
  secrets.enable = true;
};

Home Manager Modules

Located in modules/home/, these modules configure user environments.

Module Categories

Essential user configuration:
modules/home/core/
├── checks.nix           # Configuration validation
├── git.nix              # Git configuration
├── gitops.nix           # Auto-update hooks
├── secrets.nix          # User secrets
├── ssh.nix              # SSH configuration
└── shells/
    ├── fish.nix
    ├── starship.nix
    └── fastfetch.nix
Usage:
core = {
  git = {
    userName = "user";
    userEmail = "[email protected]";
  };
  shells.fish.generateCompletions = true;
  secrets.enable = true;
};

Droid Modules

Located in modules/droid/, these modules configure Nix-on-Droid environments.
modules/droid/
├── default.nix
├── nixconf.nix      # Nix configuration
└── users.nix        # User environment
Usage:
core.user.shell = pkgs.fish;

Creating Custom Modules

Basic Module Structure

A module typically follows this pattern:
modules/nixos/example/feature.nix
{
  lib,
  config,
  pkgs,
  ...
}: let
  cfg = config.example.feature;
in {
  options.example.feature = {
    enable = lib.mkEnableOption "Enable example feature";
    
    setting = lib.mkOption {
      type = lib.types.str;
      default = "default-value";
      description = "An example setting";
      example = "custom-value";
    };
  };

  config = lib.mkIf cfg.enable {
    # Your configuration here
    environment.systemPackages = with pkgs; [
      # packages
    ];
  };
}

Real Module Example

Here’s the actual hardware module from the homelab:
modules/nixos/core/hardware/default.nix
{
  lib,
  config,
  ...
}: let
  cfg = config.core.hardware;
in {
  imports = [
    ./gpu
    ./hid
    ./adb.nix
    ./bluetooth.nix
  ];

  options.core.hardware = {
    enable = lib.mkEnableOption "Enable hardware support";

    reportPath = lib.mkOption {
      type = lib.types.path;
      description = "The default report path for facter input modules";
      example = ./facter.json;
    };
  };

  config = lib.mkIf cfg.enable {
    facter = {
      inherit (cfg) reportPath;
    };
  };
}

Home Manager Module Example

The Git module with advanced options:
modules/home/core/git.nix
{
  lib,
  config,
  ...
}: let
  cfg = config.core.git;
in {
  options.core.git = {
    userName = lib.mkOption {
      type = lib.types.str;
      description = "The git username to use for this user";
      example = "john";
    };

    userEmail = lib.mkOption {
      type = lib.types.str;
      description = "The git email to use for this user";
      example = "[email protected]";
    };

    projectsDir = lib.mkOption {
      type = lib.types.path;
      description = "The directory where git projects are stored";
      default = config.home.homeDirectory + "/Documents/Projects";
      example = "/run/media/john_doe/Projects";
    };

    extraIdentities = lib.mkOption {
      type = with lib; with types;
        attrsOf (submodule {
          options = {
            directory = mkOption {
              type = str;
              description = "The directory of the group of projects";
              example = "Work";
            };
            name = mkOption {
              type = str;
              description = "The name to use for this identity";
              example = "john_work";
            };
            email = mkOption {
              type = str;
              description = "The email to use for this identity";
              example = "[email protected]";
            };
            signingKey = mkOption {
              type = str;
              description = "The SSH public key for signing commits";
              example = "ssh-ed25519 AAAA...";
            };
          };
        });
      default = {};
    };
  };

  config = {
    programs.git = {
      enable = true;
      userName = cfg.userName;
      userEmail = cfg.userEmail;
      
      signing = {
        format = "ssh";
        key = config.core.ssh.publicKey;
        signByDefault = true;
      };
      
      # Include conditional configs for extra identities
      includes = lib.mapAttrsToList (_: identity: {
        condition = "gitdir:${cfg.projectsDir}/${identity.directory}/";
        contents.user = {
          inherit (identity) name email signingKey;
        };
      }) cfg.extraIdentities;
    };
  };
}

Module Options

Common Option Types

lib.mkEnableOption
function
Creates a boolean option that defaults to false
enable = lib.mkEnableOption "Enable the feature";
lib.mkOption
function
Creates a custom option with type, default, and description
setting = lib.mkOption {
  type = lib.types.str;
  default = "value";
  description = "Description";
  example = "example";
};

Common Types

  • lib.types.bool - Boolean (true/false)
  • lib.types.str - String
  • lib.types.int - Integer
  • lib.types.path - File path
  • lib.types.package - Nix package
  • lib.types.listOf <type> - List of items
  • lib.types.attrsOf <type> - Attribute set
  • lib.types.submodule - Nested module
  • lib.types.enum [...] - One of the listed values

Conditional Configuration

Use lib.mkIf to apply configuration only when an option is enabled:
config = lib.mkIf cfg.enable {
  # This only applies when enable = true
};
Use lib.mkMerge to merge multiple conditional blocks:
config = lib.mkMerge [
  (lib.mkIf cfg.enable {
    # Always enabled config
  })
  (lib.mkIf cfg.extraFeature {
    # Extra feature config
  })
];

Module Imports

Modules can import other modules:
modules/nixos/desktop/default.nix
{
  lib,
  config,
  ...
}: let
  cfg = config.desktop;
in {
  imports = [
    ./environments
    ./features/gaming.nix
    ./features/printing.nix
    ./features/virtualisation.nix
    ./services/asusd.nix
    ./services/pipewire.nix
    ./services/flatpak.nix
  ];

  options.desktop = {
    enable = lib.mkEnableOption "Enable core desktop configurations";
  };

  config = lib.mkIf cfg.enable {
    desktop.services = {
      pipewire.enable = true;
      flatpak.enable = true;
    };
  };
}

Best Practices

  1. Use namespaces - Organize options under logical namespaces (core, desktop, userapps)
  2. Provide defaults - Always provide sensible defaults for options
  3. Document everything - Use description and example fields
  4. Use enable options - Gate configuration behind enable flags
  5. Keep modules focused - Each module should do one thing well
  6. Use conditional imports - Only import what’s needed
  7. Test modules - Test your modules on different systems

Module Discovery

The module system uses lib.discover to automatically find and import modules:
{lib, ...}: let
  modules = lib.discover ./.;
in
  modules // {
    default = {
      imports = builtins.attrValues modules;
    };
  }
This means:
  • Any .nix file in the module directory is automatically loaded
  • Subdirectories are recursively scanned
  • No manual import list to maintain

Debugging Modules

Check Module Options

List all available options:
nixos-option desktop.environments.kde

View Module Values

See the current value of an option:
nixos-option desktop.environments.kde.enable

Test Module Configuration

Build without switching:
nixos-rebuild build --flake .#system-name

Common Patterns

Feature Flag Pattern

options.feature = {
  enable = lib.mkEnableOption "Enable feature";
  
  package = lib.mkOption {
    type = lib.types.package;
    default = pkgs.feature;
    description = "Package to use";
  };
  
  extraOptions = lib.mkOption {
    type = lib.types.lines;
    default = "";
    description = "Extra configuration";
  };
};

config = lib.mkIf cfg.enable {
  environment.systemPackages = [ cfg.package ];
  # More config...
};

Multiple Choice Pattern

options.backend = lib.mkOption {
  type = lib.types.enum ["option1" "option2" "option3"];
  default = "option1";
  description = "Which backend to use";
};

config = lib.mkMerge [
  (lib.mkIf (cfg.backend == "option1") {
    # Option 1 config
  })
  (lib.mkIf (cfg.backend == "option2") {
    # Option 2 config
  })
];

Next Steps

Systems

Configure NixOS systems

Home Manager

Configure user environments

Creating Modules

Learn how to create modules

Contributing

Contribute your own modules

Build docs developers (and LLMs) love