Keystone SystemsKS Systems

Reasoning about your keystone-config

This repo is a consumer flake for Keystone. It assumes you know what a Nix flake is, roughly what a NixOS module and a Home Manager module look like, and that search.nixos.org is where you find package names. Nothing in this doc re-teaches that.

What it does cover: how Keystone's mkSystemFlake turns a small declarative inventory into a real fleet, and where the convention stops so you can drop back to vanilla Nix.

A real fleet, in one file

Here's what a four-host fleet looks like — flake.nix more or less in full:

keystone.lib.mkSystemFlake {
  admin = {
    username = "ada";
    fullName = "Ada Lovelace";
    email = "ada@example.com";
    initialPassword = "changeme";
    sshKeys = [
      "ssh-ed25519 AAAA…ada@workstation"
      "ssh-ed25519 AAAA…ada@laptop"
    ];
  };

  defaults = {
    timeZone = "America/New_York";
    updateChannel = "stable";
  };

  hostsRoot = ./hosts;

  shared = {
    # CLI tools, every user, every host (Linux + macOS).
    userModules = [
      ({ pkgs, ... }: { home.packages = with pkgs; [ fd ripgrep jq ]; })
    ];

    # System packages, OS-wide, Linux hosts only (macbook silently skips).
    systemModules = [
      ({ pkgs, ... }: { environment.systemPackages = with pkgs; [ btop ]; })
    ];

    # GUI apps, only on desktop-class hosts (laptop + workstation).
    desktopUserModules = [
      ({ pkgs, ... }: { home.packages = with pkgs; [ obsidian bitwarden-desktop ]; })
    ];
  };

  keystoneServices = {
    git.host        = "server";   # Forgejo runs on `server`; every other host
    mail.host       = "server";   # gets the matching client config wired up
    monitoring.host = "server";   # without you having to repeat yourself.
  };

  hosts = {
    workstation = { kind = "workstation"; };
    laptop      = { kind = "laptop"; };
    server      = { kind = "server"; };
    macbook     = { kind = "macbook"; };   # Home Manager only — no NixOS, no agenix
  };
}

What mkSystemFlake does with that:

  • Emits nixosConfigurations.workstation, …laptop, …server — full NixOS systems with ada as admin, the right timezone, the shared user and system modules layered on, and the relevant keystone services enabled by kind.
  • Emits homeConfigurations.macbook — Home Manager only, picking up shared.userModules but skipping the system + desktop hooks.
  • Emits packages.<system>.iso — one installer ISO that boots on any of the Linux hosts and lets ks install finish the install.
  • Reads hosts/<name>/configuration.nix and hosts/<name>/hardware.nix (Linux only) for per-host overrides, layered on top of everything above.

See flake.md for the full argument and output reference.

Scopes — the mental model

Every change you make lives in one of these scopes. Pick the scope first; the file follows.

ScopeApplies toWhere to write it
Fleet, systemOS-wide on every Linux hostshared.systemModules
Fleet, userPer-user on every host (Linux + macOS)shared.userModules
Desktop, userPer-user on laptop + workstation onlyshared.desktopUserModules
Host, systemOne Linux host's NixOShosts/<name>/configuration.nix
Host, hardwareOne Linux host's disks/CPU/firmwarehosts/<name>/hardware.nix
Host, user (macOS)A macbook hosthosts/<name>/configuration.nix
Shared infraA service whose host every other host should know aboutkeystoneServices.<service>.host
SecretAnything that mustn't land in the Nix storesecrets/<name>.age + age.secrets.*

Two questions resolve every entry: fleet vs per-host, and system vs user.

NixOS vs Home Manager

Two module systems, two namespaces. Keystone wires both up; you compose the option names for the scope you're writing in.

Module systemLives onCommon option rootsReference
NixOSLinux hostsenvironment.*, services.*, networking.*, users.*, boot.*options.nixos.org
Home ManagerEvery user, every host (including macOS)home.*, programs.<name>.*, xdg.*, wayland.*Home Manager options

The programs.<name> namespace exists in both systems with different schemas. programs.zsh.interactiveShellInit is NixOS; programs.zsh.initExtra is Home Manager. They look similar; they are not interchangeable. The host's configuration.nix is a NixOS module on Linux hosts and a Home Manager module on macOS — use the option names that match.

Installing programs

Pick the scope, drop the package in the right module hook:

# CLI everywhere → shared.userModules → home.packages
# Daemon everywhere → shared.systemModules → environment.systemPackages
# GUI on desktops → shared.desktopUserModules → home.packages
# One host only → hosts/<name>/configuration.nix → environment.systemPackages

Package names: search.nixos.org/packages.

When to drop down to vanilla Nix

mkSystemFlake returns a regular flake output set. If keystone's convention doesn't cover a case — a custom package, a check, a darwin config, an alternative test — extend the return value with //:

outputs = { keystone, ... }:
  keystone.lib.mkSystemFlake { /* … */ } // {
    packages.x86_64-linux.my-tool =
      keystone.inputs.nixpkgs.legacyPackages.x86_64-linux.callPackage ./my-tool.nix { };

    checks.x86_64-linux.my-check = /* … */;
  };

Same for inputs: declare your own nixpkgs / llm-agents / browser-previews and have keystone follow them via keystone.inputs.<name>.follows = "<name>"; in flake.nix. Comments in the scaffolded flake.nix show the canonical pattern.

The 80% case fits the helper. The other 20% is plain Nix and you have full access to it.

File layout

  • flake.nix — single mkSystemFlake call
  • hosts/<name>/ — one directory per attribute in hosts = \{ ... \}
  • hosts/<name>/configuration.nix — per-host overrides
  • hosts/<name>/hardware.nix — Linux hardware metadata (no macbook)
  • secrets/*.age + secrets.nix — agenix; see secrets/README.md
  • docs/keystone/ — these docs; edit freely

server is just an example name. Rename to anything that fits — keep the entry in hosts = \{ ... \} and the directory under hosts/ in sync.

Going deeper