1 44 Adding a Module or Profile
Nimmo edited this page 2026-09-13 08:54:02 +01:00

Adding a Module or Profile

  • Type: Runbook
  • Status: Current
  • Scope: New or extracted NixOS capability modules and composition profiles in nixos-config
  • Canonical sources: AGENTS.md; README.md (Architecture Rules and Boundary Decisions); flake.nix; modules/hardware/laptop.nix; modules/services/navidrome-mcp.nix; modules/profiles/ai-agents.nix; modules/profiles/gaming.nix; modules/profiles/backup.nix; modules/profiles/gui.nix; modules/profiles/laptop.nix; hosts/vega/default.nix; hosts/electra/default.nix; justfiles/deploy.just; Configuration Ownership Boundaries
  • Last verified: Source commit 556df88494686003b1c4f20c8e0b99b5afc16a6e (2026-09-12); source HEAD b2831d33132967b4c4fb69e6068b2b05fff8fdff changes only flake.lock. Imports, options, and commands checked 2026-09-13; no Nix file or host was changed.
  • Review triggers: Module/profile layout, NixOS option ownership, host imports, Home Manager wiring, flake builder, validation recipes, or service integration requirements

Purpose

Add one coherent capability or a deliberate bundle without changing more hosts than intended. A module or profile is a NixOS module; placing a file under modules/ does not register or activate it. Repository Structure and Import Chain shows how evaluation reaches a host, and Configuration Ownership Boundaries determines which layer should own a change.

This procedure covers the module/profile boundary. It is not the full service-migration procedure: state, secrets, ingress, backups, and cutover need the separate Service Migration and Cutover plan where applicable.

Prerequisites and safety boundary

  • Name the intended consumer hosts and specialisations. Trace their current imports before deciding a new file is necessary. If the change is a simple one-host value or a pure package helper, keep it at that narrower owner.
  • Check for an existing nixpkgs/upstream module and current repository option before defining another option with the same purpose. Adding an option is useful when multiple consumers or callers need a stable interface, not as boilerplate for every one-line import-to-enable capability.
  • For cross-cutting, service, security, or multi-host changes use the Forgejo issue/branch/merge-request lifecycle in AGENTS.md. Record the intended host/specialisation set and validation gates. Do not treat a successful evaluation as proof of a live service, route, or usable rollback.
  • Do not put secret values into module source, environment, derivations, or Home Manager-generated store files. Declare runtime SOPS delivery in the service-owning module and scope recipients separately.

Ordered procedure

1. Choose module, profile, or host-local setting

Need Use Current example
One reusable capability or policy modules/<domain>/<name>.nix modules/hardware/laptop.nix defines laptop behavior and an endurance-mode option.
A selected environment/workload made of several capabilities modules/profiles/<name>.nix profiles/laptop.nix imports GUI, Bluetooth, laptop hardware, and Wi-Fi.
An opt-in bundle with no independent configuration surface Import-to-enable profile profiles/gaming.nix sets Steam/GameMode and selected user packages when imported.
One host's factual value, device, mount, or provider URL hosts/<host>/default.nix or host-local hardware file Vega enables its Navidrome MCP module; Electra owns hardware-presence specialisations.
Pure launcher or derivation Focused helper or packages/ expression A helper returning a package is not a NixOS module solely because it is .nix.

modules/common/base.nix is not a general staging area. It reaches every registered host; add to it only when the behavior is genuinely universal. Backrest illustrates a deliberately narrower boundary: modules/profiles/backup.nix imports the service, and current desktop/server compositions select that profile.

2. Define the smallest useful interface

A simple import-to-enable module can contain configuration without declaring new options. A configurable module should declare its options in one owner and gate the actual behavior on the enable value. Current examples show both shapes:

  • modules/profiles/gaming.nix has no new enable option; selecting its import activates its Steam/GameMode and Home Manager contributions.
  • modules/hardware/laptop.nix declares laptop.enduranceMode.enable and conditionally applies its additional power-profile behavior; its base laptop utilities apply when imported.
  • modules/services/navidrome-mcp.nix declares typed nixosConfig.navidromeMcp options (enable, listener, port, URL, monitoring), uses lib.mkIf cfg.enable for the unit, SOPS credentials, firewall, and monitoring, and asserts that Navidrome itself is enabled.

Use the repository's existing option namespaces where appropriate and avoid colliding with upstream services.* or programs.* options. Give a new option a type, useful description, and safe default. Add an assertion when a dependency is required but not guaranteed by the import graph. Keep a service's state, user, listener, credentials, ordering, backup intent, and failure-monitor contribution in its owner or an explicitly composed host piece; do not hide an unreviewed network or privilege change inside a package-only module.

The flake builder already passes inputs as NixOS specialArgs; declare inputs in the module function arguments only if the module uses it. A NixOS module may contribute to home-manager.users.${config.nixosConfig.primaryUser} when system and user behavior are coupled, as the gaming and AI profiles do. Pure personal settings instead belong under home/users/nimmo/.

3. Wire only intended consumers

Add the new module to the appropriate host or profile imports list. Then set host-specific option values at the host composition root. Importing an option provider and enabling its behavior are separate steps. The current Navidrome MCP path is concrete:

hosts/vega/default.nix
  -> modules/profiles/ai-desktop.nix
     -> modules/profiles/ai-agents.nix
        -> modules/services/navidrome-mcp.nix  (declares options)
  -> nixosConfig.navidromeMcp.enable = true   (Vega activates service)

Other AI-client hosts receive the option provider through that profile but do not thereby run the Vega service. Conversely, an import-to-enable module such as profiles/gaming.nix needs no second enable assignment. Electra selects gaming in the shared imports of its igpu and dgpu specialisations, not its battery base. Those specialisations represent physical expansion-bay presence, not a request for a different workload policy; see ADR-001.

Before editing common/base.nix or a broadly imported profile, enumerate all transitive consumers. A change to profiles/gui.nix reaches both laptops; a change to common/base.nix reaches all six registered targets, including the stable VMs. Do not claim a target is included merely because its source file exists under hosts/ or potential/.

4. Stage and validate

Stage a newly created .nix file before just check: Git-backed flake evaluation ignores untracked files. Validate the exact import graph and affected host closures from the source checkout:

just check
nix build .#nixosConfigurations.<host>.config.system.build.toplevel --no-link

Replace <host> with each affected registered host. just check runs nix flake check; the host build is a non-activating check of the selected system closure. Review formatting, assertions, option merge behavior, affected hosts, and the build/package diff. If a new external flake input is involved, also follow Adding Packages and Flake Inputs for lock and update-policy work.

For a deployment-affecting change, follow Safe Testing and Deployment: test the canary with just test on the affected host, preserve Electra's active specialisation, and create an update hold before remote testing. Exercise the documented user-facing command or service path, not just the unit status. Obtain the required MR checks and live/no-op evidence before closing a multi-host or auto-update issue. Documentation-only edits need focused review, not host activation.

Success checks, rollback, and troubleshooting

  • The module/profile has one clear owner and an import path to exactly the intended hosts and specialisations; options are declared once, with appropriate defaults and dependency checks.
  • just check and targeted host builds pass. A canary, when needed, proves the command/service and client path actually work; relevant backup and monitoring behavior is checked for service changes.
  • If evaluation reports an unknown option, verify that the declaring module is imported before changing the option spelling. If a new file appears missing, confirm it is staged in Git. If a service is unexpectedly active, trace transitive imports and whether it is import-to-enable or option-gated.
  • If activation fails, use the Monitoring and Service Investigation and Safe Testing and Deployment recovery paths. Roll back the configuration through a reviewed revert or previous NixOS generation; service data/schema changes require their own recovery point and cannot be undone by a Nix generation alone.

Canonical source map

Concern Source
Repository boundaries and import chain Configuration Ownership Boundaries, Repository Structure and Import Chain
Option-gated and import-to-enable examples modules/services/navidrome-mcp.nix, modules/hardware/laptop.nix, modules/profiles/gaming.nix
Composition and selected hosts modules/profiles/ai-agents.nix, modules/profiles/laptop.nix, hosts/vega/default.nix, hosts/electra/default.nix
Validation/deployment contract AGENTS.md, justfiles/deploy.just, Safe Testing and Deployment