Appearance
Guide
nixy organizes configuration around three concepts: schema declares options, traits implement behavior, and nodes define targets.
Schema
Declare options with helpers like mkStr and mkPort. Multiple files can contribute to the same schema tree — they are deep-merged.
nix
{ mkStr, mkPort, mkBool, ... }:
{
schema.ssh = {
port = mkPort 22;
permitRoot = mkBool false;
};
schema.base = {
system = mkStr "x86_64-linux";
hostName = mkStr null;
};
}All helpers wrap their type in nullOr, so every option accepts null. See Helpers for the full list.
Traits
A trait is a named behavior unit. The key is the trait name, the value is a module (function, attrset, or path).
nix
traits.ssh = { schema, config, pkgs, ... }: {
services.openssh = {
enable = true;
ports = [ schema.ssh.port ];
settings.PermitRootLogin =
if schema.ssh.permitRoot then "yes" else "no";
};
};Nodes
Each node represents a target. It has three fields:
nix
{
nodes.server = {
traits = [ "base" "ssh" ]; # which traits to activate
schema.base.system = "x86_64-linux";
schema.ssh.port = 2222; # override schema defaults
includes = [ # extra modules
{ services.fail2ban.enable = true; }
./hardware-configuration.nix
];
};
}traits — List of trait names to activate. Referencing a non-existent name is an error.
schema — Type-checked values matching the global schema declarations. Unset values use the declared defaults. Schema values are exposed in the evaluation result alongside the module, so they can be used for routing (e.g. node.schema.base.system).
includes — Additional modules appended after trait modules.
Includes
Paths or attrsets are passed directly as modules:
nix
includes = [
./hardware-configuration.nix
{ services.fail2ban.enable = true; }
];Multi-platform
nixy doesn't prescribe how nodes are routed to builders. Use standard Nix:
nix
let
cluster = nixy.eval { inherit lib; imports = [ ./. ]; args = { inherit inputs; }; };
bySchema = pred: lib.filterAttrs (_: n: pred n.schema) cluster.nodes;
in {
nixosConfigurations = lib.mapAttrs (_: n:
lib.nixosSystem { system = n.schema.base.system; modules = [ n.module ]; }
) (bySchema (s: s.base.target or "nixos" == "nixos"));
darwinConfigurations = lib.mapAttrs (_: n:
inputs.darwin.lib.darwinSystem { system = n.schema.base.system; modules = [ n.module ]; }
) (bySchema (s: s.base.target or null == "darwin"));
}Composition with extend
extend lets you layer additional imports onto an existing evaluation, which is useful when building reusable libraries:
nix
# A library provides base traits and schema
base = nixy.eval { inherit lib; imports = [ ./base ]; };
# Consumers extend it with their own modules
mine = base.extend { imports = [ ./my-stuff ]; };This re-evaluates with the combined imports. Schema, traits, and nodes from both sources are merged.
Imports and Scanning
nixy.eval scans imports recursively:
- Directories are scanned for all
.nixfiles (recursively) - Files (
.nix) are loaded directly - Attrsets are passed through as inline modules
- Lists are flattened
By default, files starting with _ or ., plus flake.nix and default.nix, are excluded. Override with exclude:
nix
nixy.eval {
inherit lib;
imports = [ ./. ];
exclude = { name, path }: name == "test.nix";
}Cross-node References
Traits can read other nodes' schema and trait list via the nodes argument:
nix
traits.client = { schema, nodes, ... }: {
services.myApp.serverHost = nodes.server.schema.net.ip;
};Error Tracking
nixy tags each trait and include module with location information. When an evaluation error occurs, the trace includes the source (trait name or include index) and node name.