Table of contents
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 HEADb2831d33132967b4c4fb69e6068b2b05fff8fdffchanges onlyflake.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.nixhas no newenableoption; selecting its import activates its Steam/GameMode and Home Manager contributions.modules/hardware/laptop.nixdeclareslaptop.enduranceMode.enableand conditionally applies its additional power-profile behavior; its base laptop utilities apply when imported.modules/services/navidrome-mcp.nixdeclares typednixosConfig.navidromeMcpoptions (enable, listener, port, URL, monitoring), useslib.mkIf cfg.enablefor 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 checkand 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 |