Skip to content

A fleet on one page

yen is a query language for Nix configurations: one file of Nix builtins, no dependencies, no module system, no fixpoint, and no throw. You describe a fleet as data and behavior. yen answers queries on it: the systems to build, the errors, the graph of who gives what to whom, and whether your rules hold across machines.

yen has no batteries and no vocabulary. It does not know what a host, a user or NixOS is. You name your own nouns, so it configures NixOS, nix-darwin, home-manager, clusters, terraform or anything else you can model as Nix values.

Documentation · Getting started · Reference fully-featured fleet · vic/vix on yen · Sponsor vic

  • Kinds: your nouns, typed. yen.kinds.host.fields.addr = lib.types.str;
  • Scopes: where things live. spawns builds a tree from your data: a user under each host it is on.
  • Aspects: behavior, active where a predicate holds: a kind, an instance, another aspect, or any fact.
  • Sinks: where elements go. An element goes up to the nearest scope that collects it, and the sink reduces them.

Data is queried, not merged: each read is a query on the files that answer it, so content in many files never overwrites itself, and an error is a record you can read.

Two environments, three NixOS hosts, a user on two of them, postgres placed by a fact (some host names it as its database), and a rule that holds across machines. Every line runs: these files are a tested template (plus a hardware.nix stub so NixOS evaluates).

modules/kinds.nix
# Kinds are your nouns. `spawns` builds the tree from data. Sinks collect what aspects give.
{
yen,
lib,
inputs,
...
}:
let
inherit (yen.lib) same is maybe;
in
{
yen.kinds.fleet.spawns.env = _: yen.env;
yen.kinds.fleet.sinks.out.reduce = yen.lib.reduce.merge;
yen.kinds.env.fields.domain = lib.types.str;
yen.kinds.env.spawns.host = s: lib.filterAttrs (_: h: same h.env s) yen.host;
yen.kinds.host.fields = {
env = yen.kinds.env;
db = maybe yen.kinds.host;
};
yen.kinds.host.spawns.user = s: lib.filterAttrs (_: u: lib.any (same s) u.on) yen.user;
yen.kinds.host.sinks.os.nixos =
{ os, ... }:
{
imports = os;
}; # os feeds nixos
yen.kinds.host.sinks.nixos.out =
{ host, nixos, ... }:
{
nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; };
};
yen.kinds.user.fields.on = is lib.isList;
yen.flake = { out, ... }: out;
}
modules/data.nix
# Instances: plain data, refs between them.
{ yen, ... }:
{
yen.fleet.main = { };
yen.env.prod.domain = "example.com";
yen.env.lab.domain = "lab.example.com";
yen.host.web = {
env = yen.env.prod;
db = yen.host.db;
};
yen.host.db.env = yen.env.prod;
yen.host.box.env = yen.env.lab;
yen.user.ana.on = [
yen.host.web
yen.host.box
];
}
modules/behavior.nix
# Aspects are active where a predicate holds; each element goes up to the nearest `os` sink.
{ yen, lib, ... }:
let
inherit (yen.lib) same;
in
{
yen.aspects.names = {
at = yen.kinds.host;
os =
{ host, env, ... }:
{
networking = {
hostName = host.name;
domain = env.domain;
};
};
};
yen.aspects.account = {
at = yen.kinds.user; # no `os` sink on a user: the account lands on each host ana is on
os =
{ user, ... }:
{
users.users.${user.name}.isNormalUser = true;
};
};
yen.aspects.postgres = {
at = s: lib.any (h: same h.db s) (lib.attrValues yen.host); # a fact: some host uses me as its db
os.services.postgresql.enable = true;
};
# a rule across machines, checked before deploy: a host's db lives in its environment
yen.invariants.db-same-env =
{ scopes, ... }:
map (s: {
head = "bad:db-other-env:${s.name}";
pos = [ "ref:db:${s.id}:host:${s.db.name}" ];
}) (lib.filter (s: s.kind == "host" && s.db != null && !(same s.db.env s.env)) scopes);
}

Ask it. The tree comes from the data:

result
[
"fleet:main"
"fleet:main/env:lab"
"fleet:main/env:lab/host:box"
"fleet:main/env:lab/host:box/user:ana"
"fleet:main/env:prod"
"fleet:main/env:prod/host:db"
"fleet:main/env:prod/host:web"
"fleet:main/env:prod/host:web/user:ana"
]

An agent proposes a host in lab that uses the prod database. yen.lib.stage evaluates the fleet without and with the change, applies nothing, and refuses it with the rule it breaks:

changes/api-in-lab.nix
# A change, as an agent proposes it: a lab host that uses the prod database.
{ yen, ... }:
{
yen.host.api = {
env = yen.env.lab;
addr = "10.0.1.2";
db = yen.host.db;
};
}
result
{
added = [
"fleet:main/env:lab/host:api"
];
ok = false;
violations = [
"bad:db-other-env:api"
];
}
  • Graphs, Datalog and µKanren, in pure Nix: deploy waves, well-founded models with proofs and why-not, and values synthesized under constraints (a free port, guest memory that fits). Ask and prove
  • Agents as team members: every answer is JSON-safe data. Changes are staged, checked by invariants, limited by grants that only narrow, and written as change programs. An MCP server answers agents. For agents
  • Errors with fixes: code, path, got, want, fix, and near for typos. Errors as data
  • Fast where it counts: one nixpkgs per system across hosts, and trimmed NixOS module lists, with drvPath oracles that prove the systems are the same. Performance
  • Every capability of Den, den-hoag and clan, and the patterns of real fleets, each with a check, in the reference fleet. From Den
Terminal window
nix flake init -t github:vic/yen#one-page-fleet

Then read the documentation: every page is a tested example. Coming from configuration.nix, a flake or Den? The migration guides build the same systems before and after.

yen is made by vic, with love, as part of the Dendritic ecosystem. If it helps you, please sponsor vic. Issues and pull requests are welcome: see CONTRIBUTING.md.

Contribute Community Sponsor