Flake-parts and the module system
Flake-parts provides ergonomic access to the Nix module system for creating flakes.
It wraps lib.evalModules to evaluate modules with specialized abstractions for flake outputs, per-system evaluation, and module publication.
Understanding what flake-parts adds on top of the base module system is critical for working with deferred module composition architectures.
What flake-parts provides
Section titled “What flake-parts provides”Flake-parts is not a replacement for the module system, but a specialized framework that adds flake-specific conveniences on top of nixpkgs lib.evalModules.
These conveniences handle the boilerplate of creating flake outputs while preserving the composability of the underlying module system.
The perSystem abstraction
Section titled “The perSystem abstraction”The perSystem option eliminates manual iteration over system types when defining per-architecture outputs.
Instead of writing repetitive code for each system string ("x86_64-linux", "aarch64-darwin", etc.), you define packages, apps, and checks once in a perSystem module.
{ ... }:{ perSystem = { config, pkgs, system, ... }: { packages.hello = pkgs.writeShellScriptBin "hello" '' echo "Hello from ${system}" ''; };}Flake-parts automatically evaluates this module for each system listed in the systems option, producing system-specific configurations that get transposed into flake outputs like packages.<system>.hello.
The flake.modules namespace
Section titled “The flake.modules namespace”The flake.modules option creates a conventional namespace for publishing deferred modules that can be consumed by other configurations (NixOS, nix-darwin, home-manager).
This enables flakes to export modules as reusable components.
{ ... }:{ flake.modules.nixos.my-service = { config, lib, pkgs, ... }: { options.services.my-service.enable = lib.mkEnableOption "my service"; config = lib.mkIf config.services.my-service.enable { systemd.services.my-service = { /* ... */ }; }; };}Consumers can then import these modules in their own configurations:
{ inputs.our-flake.url = "github:org/repo"; outputs = { nixpkgs, our-flake, ... }: { nixosConfigurations.host = nixpkgs.lib.nixosSystem { modules = [ our-flake.modules.nixos.my-service { services.my-service.enable = true; } ]; }; };}The namespace structure is flake.modules.<class>.<name> where class identifies the module system context (nixos, darwin, homeManager, generic, flake) and name is the module identifier.
Automatic transposition
Section titled “Automatic transposition”The transposition module automatically merges per-system attributes from perSystem evaluations into conventional flake output locations.
When you define packages.hello in a perSystem module, flake-parts creates flake.packages.<system>.hello for each system without explicit configuration.
This handles the mapping from per-system configurations to the flat attribute structure expected by flake outputs.
System-specific context access
Section titled “System-specific context access”The withSystem and moduleWithSystem helpers provide escape hatches for accessing system-specific context in top-level flake modules.
These bridge the gap between flake-level (class "flake") and perSystem-level (class "perSystem") evaluation contexts.
Two-layer evaluation model
Section titled “Two-layer evaluation model”Flake-parts evaluates modules in two distinct layers, each using lib.evalModules with different module classes.
Top-level flake evaluation
Section titled “Top-level flake evaluation”The first layer evaluates modules with class "flake" to produce the overall flake structure:
# From flake-parts lib.nixlib.evalModules { specialArgs = { inherit self flake-parts-lib moduleLocation; inputs = args.inputs or self.inputs; } // specialArgs; modules = [ ./all-modules.nix module ]; class = "flake";}This evaluation creates a module system context where:
- The
flakeoption accumulates all output attributes - The
perSystemoption holds deferred modules for per-system evaluation - The
systemsoption lists architectures to enumerate - The
flake.modulesnamespace publishes modules for external consumption
The final flake outputs come from extracting config.flake after this evaluation completes.
Per-system evaluation
Section titled “Per-system evaluation”For each system in the systems list, flake-parts evaluates the deferred modules from perSystem with class "perSystem":
# From flake-parts modules/perSystem.nix(lib.evalModules { inherit modules; prefix = [ "perSystem" system ]; specialArgs = { inherit system; }; class = "perSystem";}).configEach per-system evaluation receives:
- The specific
systemstring as a module argument - System-specific input views via
inputs'andself' - Access to
config,pkgs, and other perSystem options
The allSystems option memoizes these evaluations as a mapping from system strings to perSystem configurations.
The transposition module then merges these configurations into the top-level flake outputs.
Module classes in flake-parts
Section titled “Module classes in flake-parts”Module classes prevent accidental mixing of modules from incompatible contexts.
The nixpkgs module system supports classes via the class parameter to evalModules and the _class module attribute.
Flake-parts uses two primary module classes:
"flake"- Top-level flake-parts modules that defineflake,perSystem,systemsoptions"perSystem"- Per-system modules evaluated with specific system context
The flake.modules namespace supports publishing modules for external classes:
nixos- NixOS system modulesdarwin- nix-darwin system moduleshomeManager- home-manager user modulesgeneric- Class-agnostic modules that work in any contextflake- Nested flake-parts modules
When you define flake.modules.nixos.my-module, flake-parts automatically wraps it with _class = "nixos" metadata to ensure type safety.
The deferredModule type
Section titled “The deferredModule type”The flake.modules option uses the deferredModule type to delay evaluation until the consumer calls evalModules with the appropriate class.
This type is a nixpkgs primitive, not a flake-parts invention.
The type is defined as lazyAttrsOf (lazyAttrsOf deferredModule):
- First level: module class (nixos, darwin, homeManager, generic, flake)
- Second level: module name
- Value: deferred module content
A deferred module accepts attribute sets, functions, or paths as module definitions and merges them by concatenating into a module list.
The actual evaluation happens when a consumer imports the module into their own evalModules call.
# Publishing side (flake-parts)flake.modules.nixos.example = { config, lib, ... }: { options.foo = lib.mkOption { type = lib.types.str; };};
# Consuming side (NixOS configuration)nixpkgs.lib.nixosSystem { modules = [ inputs.our-flake.modules.nixos.example { foo = "bar"; } ];}This pattern enables module composition across flake boundaries without premature evaluation.
How deferred module composition builds on flake-parts
Section titled “How deferred module composition builds on flake-parts”Deferred module composition uses flake-parts as its integration layer for producing flake outputs, but extends it with auto-discovery and structured module organization.
Key additions in deferred module composition architectures:
- import-tree for auto-discovery - Automatically discovers and imports modules from directory structures
- Namespace sharding - Organizes modules by concern (configurations, modules, packages, systems)
- Hierarchical composition - Modules at each level contribute to merged outputs
These are organizational patterns specific to aspect-based deferred module composition architectures, not features of flake-parts itself. The deferred module composition pattern fundamentally relies on deferred modules (a module system primitive), uses flake-parts for ergonomic flake integration, and adds its own conventions for module discovery and composition.
For details on the underlying module system primitives, see Module system primitives. For details on the deferred module composition pattern, see Deferred module composition.
Flake-parts is optional
Section titled “Flake-parts is optional”The deferred module composition organizational pattern does not require flake-parts.
Deferred modules are a nixpkgs primitive available through lib.evalModules, and you can evaluate them directly without any flake framework.
Flake-parts provides convenient abstractions for flake integration, but the underlying module composition mechanisms work independently.
Direct evalModules usage
Section titled “Direct evalModules usage”A minimal example demonstrating direct evaluation without flake-parts:
# services/database.nix - A deferred module{ config, lib, ... }:{ options.database = { enabled = lib.mkOption { type = lib.types.bool; default = false; description = "Enable database service"; }; port = lib.mkOption { type = lib.types.port; default = 5432; description = "Database port"; }; };
config = lib.mkIf config.database.enabled { # Service configuration would go here };}
# evaluation.nix - Direct evalModules calllet pkgs = import <nixpkgs> { }; lib = pkgs.lib;
# Evaluate modules directly result = lib.evalModules { modules = [ ./services/database.nix { database.enabled = true; } { database.port = 3306; } ]; };in{ inherit (result) config options; # result.config.database.enabled => true # result.config.database.port => 3306}This demonstrates the core composition mechanism: evalModules accepts a list of deferred modules, merges their option declarations and config definitions, computes the fixpoint where config refers to the final merged state, and returns the evaluated configuration.
What flake-parts adds
Section titled “What flake-parts adds”Flake-parts builds on this foundation with flake-specific abstractions:
- perSystem evaluation - Automatically evaluates modules for each system in
systemslist, eliminating manual per-architecture iteration - Namespace conventions -
flake.modules.<class>.<name>provides conventional publishing structure for reusable modules - Transposition - Merges per-system configurations into flat flake output schema (
packages.<system>.name) - Two-layer evaluation - Separates flake-level concerns (class
"flake") from per-system concerns (class"perSystem") - Type safety - Module classes via
_classattribute prevent mixing incompatible module contexts
When to use each approach
Section titled “When to use each approach”Use raw evalModules when:
- You need maximum control over evaluation parameters
- Your use case doesn’t fit the two-layer (flake + perSystem) model
- You’re learning module system fundamentals
- You’re building custom evaluation contexts (not flake outputs)
Use flake-parts when:
- You’re producing flake outputs with per-system variants
- You want to publish reusable modules with namespace conventions
- You need the perSystem abstraction to avoid boilerplate
- You’re integrating with the broader flake-parts ecosystem
Use deferred module composition when:
- You have complex multi-machine configurations
- You want aspect-based organization with auto-discovery
- You need hierarchical module composition across platforms
- You’re managing fleets with shared cross-cutting concerns
The choice depends on your organizational needs and whether flake-parts’ opinionated structure matches your use case.
External references
Section titled “External references”- Flake-parts documentation - Official flake-parts reference
- Nixpkgs module system - Base module system documentation
- Module classes RFC - Design rationale for module classes
- deferredModule in nixpkgs - Implementation of deferred module type