Skip to content

Caches and secrets batteries

A cache and a secret are data. The host trusts a cache, the flake asks to trust it, and CI pushes to it. A feature declares the secrets it needs, and a file lists them. Batteries write these from sinks. This fleet has one NixOS host and a repo root that writes files:

modules/fleet.nix
# Hosts build NixOS; the repo root writes files. Caches and secrets are sinks any aspect adds to.
{
yen,
lib,
inputs,
...
}:
let
b = yen.lib.batteries;
in
{
yen.kinds.repo = {
spawns.host = _: yen.host;
sinks = {
out.reduce = yen.lib.reduce.merge;
files.out = b.files.writer {
from = yen.kinds.repo.sinks.files;
inherit (inputs) files;
inherit lib;
pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
root = inputs.self;
};
};
};
yen.kinds.host.sinks.nixos.out = b.nixos.system {
from = yen.kinds.host.sinks.nixos;
inherit (inputs) nixpkgs;
};
yen.repo.main = { };
yen.host.build = { };
yen.flake = { out, ... }: out;
yen.aspects.machine = {
at = yen.kinds.host;
nixos = {
boot.loader.grub.device = "nodev";
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
};
nixpkgs.hostPlatform = "x86_64-linux";
system.stateVersion = "25.05";
};
};
}

Any aspect adds a cache to the sinks. The CI job that pushes to the cache:

modules/caches.nix
# cachix: a cache is data; the host trusts it, the flake asks to trust it, CI pushes to it.
{ yen, ... }:
let
inherit (yen.lib.batteries) cachix github files;
in
{
yen.kinds.host.sinks.caches.nixos = cachix.caches { from = yen.kinds.host.sinks.caches; }; # (1)
yen.kinds.repo.sinks.caches.out = cachix.flake-config { from = yen.kinds.repo.sinks.caches; }; # (2)
yen.aspects.caches = {
at = [
yen.kinds.host
yen.kinds.repo
];
caches = {
name = "nix-community";
publicKey = "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=";
};
};
# the CI job that pushes to the cache
yen.kinds.repo.sinks.jobs = {
reduce = yen.lib.reduce.merge;
files = github.workflow {
# (3)
from = yen.kinds.repo.sinks.jobs;
path = ".github/workflows/build.yml";
doc = {
name = "build";
on.push = { };
};
header = _: files.comment "build.yml" "Generated.";
};
};
yen.aspects.build-job = {
at = yen.kinds.repo;
jobs.build = {
runs-on = "ubuntu-latest";
steps = [
(cachix.action { name = "my-cache"; }) # (4)
{ run = "nix build .#nixosConfigurations.build.config.system.build.toplevel"; }
];
};
};
}

A cache is { name; publicKey; url ? "https://<name>.cachix.org"; }. (1) gives the host nix.settings substituters and trusted keys. (2) gives the flake nixConfig extra-substituters and keys. At (3), github.workflow writes the workflow from a sink of jobs, as on Repo batteries. (4) is a workflow step: cachix/cachix-action, which pushes what the job builds. This is the nixConfig of the flake:

result
{
extra-substituters = [
"https://nix-community.cachix.org"
];
extra-trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
}

Secrets are declared by the features that need them. The values live in a provider, never in git:

modules/secrets.nix
# secretspec.toml: each feature declares the secrets it needs (values live in a provider, never in git).
# .sops.yaml: each environment of secrets adds its creation rule.
{ yen, ... }:
let
inherit (yen.lib.batteries) secretspec secrets claim;
in
{
yen.kinds.repo.sinks = {
secretspec = {
reduce = claim { key = s: "${s.profile or "default"}/${s.name}"; }; # (1) one secret, one declaration
files = secretspec.toml {
# (2)
from = yen.kinds.repo.sinks.secretspec;
project.name = "my-app";
defaults.providers = [ "keyring" ];
};
};
sops-rules.files = secrets.sops-config { from = yen.kinds.repo.sinks.sops-rules; }; # (3)
};
yen.aspects.database = {
at = yen.kinds.repo;
secretspec = {
name = "DATABASE_URL";
description = "PostgreSQL connection string";
};
sops-rules = {
path_regex = "secrets/db/.*";
age = "age1db";
};
};
yen.aspects.database-dev = {
at = yen.kinds.repo;
secretspec = {
name = "DATABASE_URL";
profile = "development";
required = false;
default = "sqlite://./dev.db";
};
};
}

A secret is { name; profile ? "default"; … } with any secretspec field. At (1), claim is the reduce: a secret that two scopes declare is an error. (2) writes secretspec.toml. (3) writes .sops.yaml from a sink of creation rules. These are the files:

[defaults]
providers = ["keyring"]
[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string" }
[profiles.development]
DATABASE_URL = { default = "sqlite://./dev.db", required = false }
[project]
name = "my-app"
"creation_rules":
-
"age": "age1db"
"path_regex": "secrets/db/.*"
Battery Value of Gives
cachix.caches { from; } a NixOS, nix-darwin or home-manager key of a sink of caches nix.settings substituters and trusted keys
cachix.flake-config { from; } a flake-level key flake nixConfig extra-substituters and keys
cachix.action { name; authToken; version; } a workflow step cachix/cachix-action, which pushes what the job builds
secretspec.toml { from; project; defaults; providers; profiles; } a key that gives to the files sink secretspec.toml
secrets.sops-config { from; path; } the same .sops.yaml from a sink of creation rules
ContributeCommunitySponsor