Skip to main content
This guide covers creating custom modules for your homelab infrastructure. Modules are the building blocks that make your configuration reusable and maintainable.

Module Structure

Modules in this homelab are organized by scope:
  • modules/nixos/ - System-level NixOS modules
  • modules/home/ - User-level Home Manager modules
  • modules/droid/ - Nix-on-Droid modules
Each category is further organized by function:
modules/nixos/
├── core/           # Essential system functionality
├── desktop/        # Desktop environments and features
└── hosting/        # Server and hosting capabilities

modules/home/
├── core/           # Essential user configuration
└── userapps/       # User applications

Creating a Module

1

Create Issue

Follow the Issue-First policy:
# Create issue: "feat: add bluetooth module for hardware support"
2

Create Feature Branch

git checkout -b dev-add-bluetooth-module
3

Choose Module Location

Decide where your module belongs based on its scope:
  • System hardware? → modules/nixos/core/hardware/
  • Desktop feature? → modules/nixos/desktop/features/
  • User application? → modules/home/userapps/
4

Create Module File

Create a new .nix file in the appropriate directory:
touch modules/nixos/core/hardware/bluetooth.nix
5

Write Module Code

Follow the standard module pattern:
modules/nixos/core/hardware/bluetooth.nix
{
  lib,
  config,
  pkgs,
  ...
}: let
  cfg = config.core.hardware.bluetooth;
in {
  options.core.hardware.bluetooth = {
    enable = lib.mkEnableOption "Enable bluetooth hardware support";
    
    # Additional options
    powerOnBoot = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Power on bluetooth adapter on boot";
    };
  };

  config = lib.mkIf cfg.enable {
    hardware.bluetooth = {
      enable = true;
      powerOnBoot = cfg.powerOnBoot;

      settings = {
        General = {
          Experimental = true;
          Enable = "Source,Sink,Media,Socket";
        };
      };
    };
    
    # Optional: Add packages
    environment.systemPackages = with pkgs; [
      bluez
    ];
  };
}
6

Register Module

Add your module to the appropriate default.nix file:
modules/nixos/core/hardware/default.nix
{
  imports = [
    ./bluetooth.nix
    ./adb.nix
    ./gpu
    ./hid
  ];
}
7

Validate Module

Test that your module compiles:
nix flake check
8

Commit Changes

Use conventional commits:
git add modules/nixos/core/hardware/bluetooth.nix
git commit -m "feat: add bluetooth hardware module"

Module Patterns

Basic Enable Module

Simplest form - just an enable option:
{
  lib,
  config,
  ...
}: let
  cfg = config.desktop.features.gaming;
in {
  options.desktop.features.gaming = {
    enable = lib.mkEnableOption "Enable steam integration";
  };

  config = lib.mkIf cfg.enable {
    environment.systemPackages = with pkgs; [
      moonlight-qt
    ];
  };
}

Module with Options

Add configuration options:
{
  lib,
  config,
  ...
}: let
  cfg = config.core.secrets;
in {
  options.core.secrets = {
    enable = lib.mkEnableOption "Enable the core secrets module";

    defaultSopsFile = lib.mkOption {
      type = lib.types.nullOr lib.types.path;
      default = null;
      description = ''
        The default secrets file to use for the secrets module.
        This is used when no specific secrets file is provided.
      '';
      example = ./secrets.yaml;
    };
  };

  config = lib.mkIf cfg.enable {
    sops.defaultSopsFile = lib.mkIf (cfg.defaultSopsFile != null) cfg.defaultSopsFile;
  };
}

Nested Module Structure

For complex features, use nested modules:
modules/nixos/desktop/environments/
├── default.nix
├── kde.nix
├── cosmic.nix
├── display_managers/
│   ├── default.nix
│   ├── sddm.nix
│   └── greetd/
│       └── default.nix
└── managers/
    ├── default.nix
    └── hyprland.nix

Home Manager Module

User-level modules follow similar patterns:
{
  lib,
  config,
  pkgs,
  ...
}: let
  cfg = config.userapps.browsers.firefox;
in {
  options.userapps.browsers.firefox = {
    enable = lib.mkEnableOption "Enable Firefox browser";
  };

  config = lib.mkIf cfg.enable {
    programs.firefox = {
      enable = true;
      # Additional Firefox configuration
    };
  };
}

Module Best Practices

  • Use descriptive, lowercase names
  • Separate words with hyphens in filenames: bluetooth.nix, network-manager.nix
  • Use dot notation in option paths: core.hardware.bluetooth
  • Always use lib.mkEnableOption for boolean enables
  • Provide sensible defaults
  • Include descriptions for all options
  • Add example values where helpful
  • Use proper types: lib.types.str, lib.types.int, lib.types.path, etc.
  • Group related configuration together
  • Use let ... in for computed values
  • Keep config section clean and readable
  • Use lib.mkIf to conditionally apply configuration
  • Declare module inputs: lib, config, pkgs
  • Import other modules through default.nix files
  • Avoid circular dependencies

Testing Your Module

Local Testing

Test on a system configuration:
systems/test-system/default.nix
{
  core.hardware.bluetooth.enable = true;
}

Flake Checks

Ensure it passes validation:
nix flake check

Manual Validation

Build the configuration:
nix build .#nixosConfigurations.test-system.config.system.build.toplevel

Common Module Types

Hardware Module

Enables hardware support:
core.hardware.bluetooth.enable = true;
core.hardware.gpu.dedicated.nvidia.enable = true;

Service Module

Configures system services:
core.networking.tailscale.enable = true;
core.networking.openssh.enable = true;

Application Module

Installs and configures applications:
userapps.browsers.firefox.enable = true;
userapps.development.editors.vscode.enable = true;

Next Steps

Build docs developers (and LLMs) love