Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started

A step-by-step first flake: create it, add a library overlay, register a module, use an integration, then consume your flake from a second flake. The finished shape of each step also exists as a working flake under examples/literate-flake/ in the repository, with commentary.

1. A minimal caisson flake

Create a directory with this flake.nix:

{
  description = "my first caisson flake";

  inputs = {
    caisson.url = "github:nix-caisson/caisson";
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
  };

  outputs =
    inputs@{ caisson, ... }:
    let
      lib = caisson.lib.caisson.mkLib {
        inherit (caisson.lib.caisson.pins.flake inputs) sources root;
        name = "my-flake";
        systems = [ "x86_64-linux" ];
        projects = {
          inherit caisson;
        };
        configs = lib: lib.caisson.mkModules ./configs;
      };
    in
    lib.caisson.flake-parts.mkTopConfiguration { };
}

lib.caisson.mkLib composes a library from the lib of nixpkgs, the functions and registries of caisson under lib.caisson (where each integration also has a namespace), and the overlays you register. name is the project’s name: the flake configuration takes it as its name, and your overlays contribute to the composed library under it (lib.my-flake). systems, the platforms the flake builds for, is declared here too; flake-parts reads it from the composition. Consuming caisson as a project registers everything it exports, its integrations included, which contributes lib.caisson (a namespace per integration target); lib.caisson.flake-parts.mkTopConfiguration then evaluates flake-parts with that library and your config module, the configuration registered under the project’s name (configs/flake/my-flake). flake-parts comes from the flake-parts input of the flake, like every ecosystem caisson wraps: the integration calls that source with the composed library, so the evaluation runs on the same nixpkgs lib the rest of the composition does.

The config module is the top-level configuration of the flake, and it is registered. lib.caisson.mkModules ./configs reads configs/<class>/<name>/default.nix into the configs registration, and the configuration is then lib.caisson.configs.flake.my-flake. Create configs/flake/my-flake/default.nix:

{ ... }:
{ pkgs, ... }:
{
  perSystem =
    { pkgs, ... }:
    {
      packages.default = pkgs.hello;
    };
}

Note the two argument lists: every registered file takes the closure attrset ({ closure-inputs, ... }) first, then its ordinary module arguments. That convention is the subject of Closed inputs.

Check it:

nix flake check
nix build

2. Add a library overlay

An overlay contributes a namespace to the composed library. Create lib-overlays/default/default.nix:

{ ... }:
{
  imports = [ ];
  overlay = final: prev: {
    my-flake = (prev.my-flake or { }) // {
      greet = name: "hello, ${name}";
    };
  };
}

Register it in flake.nix and export it, and turn on the lib export in the config module. lib.caisson.mkLibOverlays ./lib-overlays reads lib-overlays/<name>/default.nix into the libOverlays registration:

      lib = caisson.lib.caisson.mkLib {
        inherit (caisson.lib.caisson.pins.flake inputs) sources root;
        name = "my-flake";
        systems = [ "x86_64-linux" ];
        projects = {
          inherit caisson;
        };
        configs = lib: lib.caisson.mkModules ./configs;
        libOverlays = lib: lib.caisson.mkLibOverlays ./lib-overlays;
      };
  caisson = {
    libOverlays.exported = libOverlays: { inherit (libOverlays) default; };
  };

Now lib.my-flake.greet is available everywhere the composed library flows: in the config module, in registered modules, and to consumers in the lib output of the flake, which is the composed library. Use it in perSystem:

      packages.default = pkgs.writeText "greeting" (lib.my-flake.greet "Nix");

3. Register a module

Modules are class-keyed: flake modules feed flake-parts, and integration classes (nixos, homeManager, …) feed their module systems. lib.caisson.mkModules ./modules reads modules/<class>/<name>/default.nix into the registration, the first directory level being the class. Register a flake-class module by creating its directory:

      lib = caisson.lib.caisson.mkLib {
        inherit (caisson.lib.caisson.pins.flake inputs) sources root;
        name = "my-flake";
        systems = [ "x86_64-linux" ];
        projects = {
          inherit caisson;
        };
        modules = lib: lib.caisson.mkModules ./modules;
        configs = lib: lib.caisson.mkModules ./configs;
        libOverlays = lib: lib.caisson.mkLibOverlays ./lib-overlays;
      };

modules/flake/default/default.nix:

{ ... }:
{ ... }:
{
  perSystem =
    { pkgs, ... }:
    {
      devShells.default = pkgs.mkShell { packages = [ pkgs.nixfmt ]; };
    };
}

mkConfiguration applies the selected flake-class modules alongside the config module: moduleImports returns the list to apply, like libOverlayImports, and when it is omitted every registered entry named default applies, here this flake’s default and caisson/default (the default module of caisson, which carries the nixpkgs integration’s module layer). The entries named core apply to every evaluation of the class regardless. Module classes covers registration, selection, and export.

4. Use an integration

Integrations bring the same conventions to other module ecosystems and take their ecosystem as an explicit ecosystemSrc, a function of the tree’s sources. A NixOS configuration takes its systems and its package set from the composition: it is evaluated per system, once for each system mkLib declares, on the package config named default, which the mkLib call declares with pkgSets = lib: { default = lib.caisson.nixpkgs.mkConfiguration { }; };. With the single system declared above, mkTopConfiguration returns the evaluated configuration. At the top level of the config module:

  flake.nixosConfigurations.example = lib.caisson.nixos.mkTopConfiguration {
    ecosystemSrc = sources: sources.nixpkgs;
    configModule =
      { ... }:
      {
        boot.loader.grub.enable = false;
        fileSystems."/" = {
          device = "none";
          fsType = "tmpfs";
        };
        system.stateVersion = "25.05";
      };
  };

Instead of passing ecosystemSrc at every call, the mkLib call can declare a default for the nixos key, the key a NixOS configuration reads (defaultEcosystemSrc = sources: { nixos = sources.nixpkgs; };), and the argument can be dropped; an explicit argument still wins, and the source named exactly nixos in the sources passed to mkLib is the last fallback. See Ecosystem sources.

With caisson consumed as a project, its integration overlays are already registered and applied, so caisson.nixos is present. To compose only some of them, keep the project registration and select per item over the combined dictionary:

        libOverlayImports =
          lib: with lib.caisson; [
            libOverlays."caisson/flake-parts"
            libOverlays."caisson/nixos"
            libOverlays.default
          ];

The library reference documents the integration namespaces.

5. Consume your flake from another flake

A consumer registers your exported overlay the same way:

{
  inputs = {
    caisson.url = "github:nix-caisson/caisson";
    my-flake.url = "github:you/my-flake";
  };

  outputs =
    inputs@{ caisson, my-flake, ... }:
    let
      lib = caisson.lib.caisson.mkLib {
        inherit (caisson.lib.caisson.pins.flake inputs) sources root;
        name = "consumer";
        systems = [ "x86_64-linux" ];
        projects = {
          inherit caisson my-flake;
        };
        configs = lib: lib.caisson.mkModules ./configs;
      };
    in
    lib.caisson.flake-parts.mkTopConfiguration { };
}

The consumer’s composed library now has lib.my-flake.greet: the project registration brings in the overlays you exported, each overlay’s imports chain guarantees anything it depends on composes with it, and your exported modules land in the consumer’s registry under my-flake/<name>, selectable at each use site. A consumer who wants only part of your project selects with libOverlayImports, as shown above; the my-flake.modules.<class>.<name> flake outputs serve consumers who import modules without composing anything.

Where next

  • Closed inputs, the convention every registered file follows.
  • How lib is composed: the whole composition pass, and caisson-core, the layer lib.caisson is built on.
  • Testing, including callConsumerFlake for testing consumer flakes without a push/lock cycle.
  • FAQ for the questions this page tends to raise.