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
libis composed: the whole composition pass, and caisson-core, the layerlib.caissonis built on. - Testing, including
callConsumerFlakefor testing consumer flakes without a push/lock cycle. - FAQ for the questions this page tends to raise.