/architecture

Two repositories, one pinned release.

An opinionated NixOS for one person on one machine. One disk layout, one user, four desktops, two profiles. This page shows how the pieces fit, from the installer's first screen to the modules a machine is built from.

01The pieces

Installing and configuring are separate jobs.

installer

ephemeral-nixos

The ISO, the installer's screens, install.sh and the install test. It has no NixOS modules of its own. Its flake.nix is the machine flake that gets installed.

config

ephemeral-nixos-config

Every NixOS and Home Manager module a machine gets, the partition layout, the helper commands and their tests. The only place modules live.

machine

/persist/etc/nixos

flake.nix pinning a config release, plus settings.json, hardware-configuration.nix, apps.json and your own modules/.

The split means the installer can't drift from the system it installs: the partition layout, the modules and the locked nixpkgs all come from the one config release it pins. An installed machine never needs the installer again. It follows config releases on its own, through ephemeral-update. A change to the config reaches new installs only once the installer pins a release that has it, and the installer only moves to a release after its install test passes with it.

02Install pipeline

From the ISO to a running desktop.

Each step links to the file that implements it.

  1. 01you

    Boot the Ephemeral Nix ISO

    Or the NixOS ISO with a verified clone. UEFI, Secure Boot off.

  2. 02ephemeral-nixosinstaller/

    Installer screens

    A Go program (Bubble Tea) asks one question per screen, checks each answer with the same rules as the scripts, and ends on a review.

  3. 03ephemeral-nixosscripts/install.sh

    install.sh

    Does all the disk work, in stages. Nothing outside /tmp is written until the disk’s name has been typed.

    1. prepare
    2. confirm
    3. partition
    4. hardware
    5. install
  4. 04/persist/etc/nixosflake.nix

    The machine’s flake

    flake.nix pinning a config release, settings.json with your answers, hardware-configuration.nix and apps.json. It is built in a temporary folder and evaluated before the disk is touched, then written to /persist/etc/nixos, with a read-only copy in /persist/etc/nixos-install.

  5. 05ephemeral-nixos-configmodules/_disko-layout.nix

    Disk layout (disko)

    The partition layout comes from the pinned config release. GPT, a 1G ESP, then LUKS2 with argon2id holding btrfs subvolumes.

  6. 06ephemeral-nixos-configflake/export.nix

    lib.mkFlake

    Merges the machine’s inputs over the config’s, and loads every module in the config’s modules/ and the machine’s own modules/ with import-tree.

  7. 07ephemeral-nixos-configmodules/host-builder.nix

    Settings, then aspects

    settings.json is checked against its schema on its own first. host-builder turns the typed settings into a list of aspects for nixosSystem.

  8. 08the installed system

    NixOS and Home Manager desktop

    nixos-install builds it and installs systemd-boot. From then on, every boot resets / and every ephemeral-update rebuilds from the machine’s flake.

After the install, ephemeral-update repeats steps 06 to 08 from /persist/etc/nixos.
The install pipeline: eight steps from booting the ISO to the installed desktop, colour-coded by which repository owns each step.

03Disk

One layout, encrypted below the filesystem.

diskDeviceGPT
ESP1G · vfat

/boot

kernel, initrd, systemd-boot. Mounted umask=0077. Not encrypted.

LUKS2 · cryptrootrest of disk · argon2id
btrfs · label nixoscompress=zstd, noatime
  • root/deleted and recreated at every bootreset
  • root-blanknot mountedread-only, empty snapshot of roottemplate
  • nix/nixthe store; rebuilt by Nix, never resetkept
  • persist/persist/home and every declared pathkept
  • log/var/logjournal and logs, across bootskept
  • swap/.swapvolswap file, inside the encryptionkept
The layout every install gets, from modules/_disko-layout.nix. The disk test formats a VM disk with it and checks it. Changing any part of it is a breaking change.

Everything but the ESP is inside LUKS2, the swap file included, so nothing on the system subvolumes is readable without the passphrase. Hibernation is off, because protecting the kernel image rules it out; suspend works. TRIM is passed through the encryption, which shows which blocks are in use but not what's in them.

04Releases

A machine only ever builds a released tag.

The installer's flake.nix pins a config tag, and its flake.lock pins the rest, so the same installer release always installs the same system. Installer v3.7.6 installs configv4.0.0.

On the machine, ephemeral-update checks for a newer config release before it rebuilds. It only offers a newer, well-formed tag, shows the changelog since yours, names a new major version before asking, and on yes changes the tag and updates the lock. Offline, it rebuilds what's pinned.

Releases are immutable. The config gets a patch release every Monday with the newest nixpkgs once the boot tests pass; other inputs, such as Home Manager and Stylix, come in a pull request a person reads first, because their code runs as root on every machine. Latest: config v4.1.0, installer v3.7.6.

ephemeral-nixos/flake.nix
{
  description = "An Ephemeral Nix machine";

  inputs = {
    ephemeral.url = "github:the-wittch/ephemeral-nixos-config/v4.0.0";
    # the installer runs nixpkgs tools by name
    nixpkgs.follows = "ephemeral/nixpkgs";
  };

  outputs =
    inputs:
    inputs.ephemeral.lib.mkFlake {
      inherit inputs;
      root = ./.;
    };
}

05Dendritic modules

One feature, one file. No imports between them.

The config follows the dendritic pattern. Every .nix file under modules/ is a flake-parts module, loaded by import-tree; paths containing /_ are skipped, and that's the only exclusion. There is no import list to keep up to date.

A feature's NixOS config, Home Manager config, persistence, permissions and sandbox live in its own file. Files don't import each other by path. They add to shared modules, called aspects: flake.modules.nixos.<name> and flake.modules.homeManager.<name>.

An aspect that needs another imports it through config.ephemeral.aspects, which wraps each one with a key, so importing it from several places applies it once. The aspect names are a public interface: your machine's modules add to them, and renaming one is a breaking change.

modules/
modules/
├── boot.nix              # systemd-boot, plymouth, tmpfs /tmp
├── ephemeral-root.nix    # the rollback unit and root-reset-check
├── persist.nix           # impermanence and the base kept paths
├── hardening.nix         # kernel parameters and sysctls
├── opensnitch.nix        # one feature: its config and its kept paths
├── host-builder.nix      # the only place a machine is composed
├── aspects.nix           # shared modules, each with a key
├── settings/_schema.nix  # every setting and its default
├── desktop/              # gnome, plasma, niri, umbriel, noctalia
├── dotfiles/             # Home Manager: zsh, git, starship, neovim…
└── apps/                 # the ephemeral-apps catalog, by category

06Profiles and settings

Typed settings in, a list of aspects out.

Settings are evaluated once, on their own, before anything is composed. Their options and defaults are in settings/_schema.nix and nowhere else; a wrong value, such as "desktop": "kde", is reported with the allowed values. Then host-builder.nix, the only place a machine is composed, picks the aspects:

settings.jsonchecked against settings/_schema.nixhost-builder.nix
WhenAspects addedWhat they bring
alwaysbaseboot, disk layout, root reset, persistence, users, hardening, networking, Nix settings
alwayspcimports base; adds audio, fonts, printing, Flatpak with Flathub, Podman, the wallpaper
profile = fullfull<desktop>Home Manager, Stylix, snapshots, Syncthing, SMB, scanning, disk health, nix-ld, direnv
profile = minimalminimalminimal-<desktop>a plain desktop, no Home Manager; finish it later with ephemeral-setup
settingsnvidialaptopfingerprinttailscaleeach when its setting is on
useOpenSnitch, full onlyopensnitchon by default; outbound connections need your approval
settings, full onlyextrascontroldpassSshAgentignored on the small install, and the rebuild warns that they are
apps.json, full only<app>.nixoseach app picked in ephemeral-apps
your modules/…every file in /persist/etc/nixos/modules, adding to any aspect
nixpkgs.lib.nixosSystem → nixosConfigurations.nixos
How a machine is composed from its settings: the aspects added in each case.

full is the default. minimal is the installer's small install: a plain desktop to start from, turned into the full system later with ephemeral-setup. A full and a minimal variant of the same desktop have to agree on the login manager, keyring, wallet and persisted folders; desktop-parity checks it. Every setting is on settings.json.

07Declaring persistence

What a feature keeps is in the feature's file.

Everything that has to survive a reboot is listed in environment.persistence."/persist", from the impermanence module, by the feature that needs it. OpenSnitch keeps its rules; Tailscale its state; GNOME its login manager's.

The full list is collected at build time into /etc/ephemeral/kept, which ephemeral-doctor kept prints. The boot test reads the same list and checks each entry survives a reboot, so a path that's declared but not actually kept fails CI.

Persistence in detail

modules/opensnitch.nix (excerpt)
{
  flake.modules.nixos.opensnitch =
    { lib, pkgs, ... }:
    {
      environment.persistence."/persist".directories = [ "/var/lib/opensnitch" ];
      environment.systemPackages = [ pkgs.opensnitch-ui ];

      services.opensnitch = {
        enable = true;
        settings = {
          DefaultAction = "deny";
          InterceptUnknown = true;
        };
      };
    };
}

08The rules

Written down, and checked against.

Both repositories keep an ARCHITECTURE.md listing the rules every change is held to: modules, the ephemeral root, disk and encryption, security, helper commands, releases, tests and docs. A change that breaks one needs a reason, written in the pull request and in that file.

Config ARCHITECTURE.md · Installer ARCHITECTURE.md