Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nixy

A minimal NixOS/Darwin/Home Manager framework.

What is Nixy?

Nixy helps you organize NixOS configurations around nodes (machines) and modules (reusable features). Instead of managing complex module imports, you declare what each machine needs:

nodes.server = {
  system = "x86_64-linux";
  base.enable = true;
  ssh.enable = true;
};

Key Features

  • Node-centric: One node = one machine. All config in one place.
  • Conditional modules: Only enabled modules are imported.
  • Type-safe: Options on disabled modules throw errors.
  • Multi-platform: NixOS, Darwin, and Home Manager support.
  • Dependency tracking: Modules can declare requirements.

Quick Start

nix flake init -t github:anialic/nixy#minimal

Then edit nodes/ and modules/ to match your setup.

Getting Started

Installation

Initialize a new project from a template:

nix flake init -t github:anialic/nixy#minimal

Available templates:

  • minimal - Single NixOS machine
  • multi-platform - NixOS + Darwin + Home Manager
  • deploy-rs - Remote deployment
  • without-flakes - Traditional setup

Project Structure

my-config/
├── flake.nix
├── modules/
│   └── base.nix
└── nodes/
    └── my-machine.nix

Basic Configuration

flake.nix

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  inputs.nixy.url = "github:anialic/nixy";

  outputs = { nixpkgs, nixy, ... }@inputs: nixy.mkFlake {
    inherit nixpkgs;
    imports = [ ./. ];
    args = { inherit inputs; };
  };
}

modules/base.nix

{ mkStr, ... }:
{
  modules.base = {
    target = "nixos";
    options.hostName = mkStr null;
    module = { node, ... }: {
      networking.hostName = node.base.hostName;
      system.stateVersion = "24.11";
    };
  };
}

nodes/my-machine.nix

{
  nodes.my-machine = {
    system = "x86_64-linux";
    base.enable = true;
    base.hostName = "my-machine";
  };
}

Building

nixos-rebuild switch --flake .#my-machine

Configuration

mkFlake / mkConfiguration

nixy.mkFlake {
  nixpkgs;          # required - nixpkgs input
  imports ? [ ];    # directories or files to scan
  args ? { };       # passed to all modules
  exclude ? null;   # filter function
}

imports

Accepts paths, directories, or inline modules:

imports = [
  ./.           # scan current directory
  ./modules     # scan specific directory
  ./special.nix # single file
  { ... }       # inline module
];

args

Passed to framework modules and NixOS/Darwin/HM modules:

args = { inherit inputs; myCustomArg = "value"; };

exclude

Filter scanned files. Default excludes _*, .*, flake.nix, default.nix:

exclude = { name, path }:
  name == "test.nix" || lib.hasPrefix "_" name;

Top-level Options

OptionDescription
systemsTarget systems (default: x86_64/aarch64 linux/darwin)
modules.*Module definitions
nodes.*Node definitions
targets.*Target builders
rulesBuild-time assertions
perSystemPer-system outputs
flake.*Extra flake outputs

Modules

Definition

{ mkStr, mkBool, ... }:
{
  modules.myModule = {
    target = "nixos";           # "nixos", "darwin", "home", or null (all)
    requires = [ "base" ];      # dependencies
    options = {
      setting = mkStr "default";
    };
    module = { node, ... }: {
      # NixOS/Darwin/HM config
    };
  };
}

Fields

target

Restricts module to specific platforms:

  • "nixos" - NixOS only
  • "darwin" - nix-darwin only
  • "home" - Home Manager only
  • null - All platforms

requires

List of module names that must be enabled:

modules.desktop = {
  requires = [ "base" "gui" ];
  # ...
};

Build fails if requirements aren’t met.

options

Option declarations using helpers or standard lib.mkOption:

options = {
  name = mkStr null;
  enabled = mkBool true;
  ports = mkList lib.types.port [ 80 443 ];
};

module

NixOS/Darwin/HM module. Receives special arguments:

ArgumentDescription
nodeCurrent node’s clean config
nodesAll nodes’ clean configs
nameNode name
systemSystem string

Splitting Modules

Same-name modules across files are merged:

# modules/base/base.nix
modules.base.options.hostName = mkStr null;
modules.base.module = { node, ... }: {
  networking.hostName = node.base.hostName;
};

# modules/base/boot.nix
modules.base.module = {
  boot.loader.systemd-boot.enable = true;
};

Nodes

Definition

{
  nodes.myMachine = {
    system = "x86_64-linux";
    base.enable = true;
    base.hostName = "my-machine";
  };
}

Fields

system (required)

Target system: "x86_64-linux", "aarch64-linux", "aarch64-darwin", "x86_64-darwin"

target

Override inferred target. Usually automatic:

  • *-darwin"darwin"
  • *-linux"nixos"

Set explicitly for Home Manager:

nodes."user@host" = {
  system = "x86_64-linux";
  target = "home";
  # ...
};

extraModules

Additional NixOS/Darwin/HM modules:

nodes.server = {
  # ...
  extraModules = [
    { services.openssh.enable = true; }
    ./hardware-configuration.nix
  ];
};

instantiate

Custom builder function:

nodes.stable-server = {
  system = "x86_64-linux";
  instantiate = { system, modules, specialArgs }:
    inputs.nixpkgs-stable.lib.nixosSystem {
      inherit system modules specialArgs;
    };
  # ...
};

Module Options

Enable modules and set options:

nodes.desktop = {
  system = "x86_64-linux";
  
  base.enable = true;
  base.hostName = "desktop";
  base.timeZone = "America/New_York";
  
  gui.enable = true;
  gui.driver = "nvidia";
};

Setting options without enable = true throws an error.

Option Helpers

Helpers simplify option declarations. All helpers are available as framework module arguments.

Basic Types

mkStr

String option. Pass null for optional (defaults to null), or a value for required with default.

{ mkStr, ... }:
{
  modules.web.options = {
    domain = mkStr null;           # optional, default null
    protocol = mkStr "https";      # required, default "https"
  };
}

mkBool

Boolean option.

{ mkBool, ... }:
{
  modules.service.options = {
    enabled = mkBool true;
    debug = mkBool false;
  };
}

mkInt

Integer option.

{ mkInt, ... }:
{
  modules.cache.options = {
    maxSize = mkInt 1024;
    ttl = mkInt null;    # optional
  };
}

mkPort

Port number (1-65535).

{ mkPort, ... }:
{
  modules.server.options = {
    httpPort = mkPort 80;
    httpsPort = mkPort 443;
  };
}

mkPath

Path option.

{ mkPath, ... }:
{
  modules.backup.options = {
    destination = mkPath /var/backup;
    source = mkPath null;
  };
}

mkLines

Multi-line string, lines are concatenated.

{ mkLines, ... }:
{
  modules.nginx.options = {
    extraConfig = mkLines "";
  };
}

# Usage in node:
nodes.web.nginx.extraConfig = ''
  gzip on;
  gzip_types text/plain application/json;
'';

mkAttrs

Attribute set (untyped).

{ mkAttrs, ... }:
{
  modules.app.options = {
    environment = mkAttrs { };
  };
}

# Usage:
nodes.server.app.environment = {
  NODE_ENV = "production";
  PORT = "3000";
};

Collections

mkList

List with element type and default value.

{ mkList, lib, ... }:
{
  modules.firewall.options = {
    allowedPorts = mkList lib.types.port [ 22 80 443 ];
    blockedIPs = mkList lib.types.str [ ];
  };
}

mkListOf

List with element type, empty default.

{ mkListOf, lib, ... }:
{
  modules.users.options = {
    admins = mkListOf lib.types.str;      # default: [ ]
    keys = mkListOf lib.types.path;
  };
}

mkAttrsOf

Attribute set with typed values, empty default.

{ mkAttrsOf, lib, ... }:
{
  modules.dns.options = {
    records = mkAttrsOf lib.types.str;    # default: { }
  };
}

# Usage:
nodes.ns.dns.records = {
  "example.com" = "192.168.1.1";
  "api.example.com" = "192.168.1.2";
};

mkStrList

Shorthand for mkListOf lib.types.str.

{ mkStrList, ... }:
{
  modules.git.options = {
    allowedUsers = mkStrList;    # default: [ ]
  };
}

Choice Types

mkEnum

One of predefined values.

{ mkEnum, ... }:
{
  modules.log.options = {
    level = mkEnum [ "debug" "info" "warn" "error" ] "info";
  };
}

mkEither

One of two types.

{ mkEither, lib, ... }:
{
  modules.proxy.options = {
    upstream = mkEither lib.types.str lib.types.port "localhost";
  };
}

# Usage:
nodes.proxy.proxy.upstream = 8080;        # port
nodes.proxy.proxy.upstream = "backend";   # string

mkOneOf

One of multiple types.

{ mkOneOf, lib, ... }:
{
  modules.config.options = {
    value = mkOneOf [ lib.types.str lib.types.int lib.types.bool ] "";
  };
}

Advanced Types

mkPackage

Package option, no default (must be set).

{ mkPackage, ... }:
{
  modules.editor.options = {
    package = mkPackage;
  };
}

# Usage:
nodes.dev.editor.package = pkgs.neovim;

mkPackageOr

Package option with default.

{ mkPackageOr, pkgsFor, ... }:
let pkgs = pkgsFor "x86_64-linux";
in {
  modules.shell.options = {
    package = mkPackageOr pkgs.bash;
  };
}

mkRaw

Raw type (any value), no default.

{ mkRaw, ... }:
{
  modules.custom.options = {
    builder = mkRaw;
  };
}

mkRawOr

Raw type with default.

{ mkRawOr, ... }:
{
  modules.hook.options = {
    preStart = mkRawOr null;
  };
}

mkNullable

Any type, nullable with null default.

{ mkNullable, lib, ... }:
{
  modules.db.options = {
    port = mkNullable lib.types.port;     # null or port
    host = mkNullable lib.types.str;
  };
}

Submodules

mkSub

Nested options as submodule.

{ mkSub, mkStr, mkPort, ... }:
{
  modules.database.options = {
    connection = mkSub {
      host = mkStr "localhost";
      port = mkPort 5432;
      name = mkStr null;
    };
  };
}

# Usage:
nodes.app.database.connection = {
  host = "db.example.com";
  port = 5432;
  name = "myapp";
};

mkSubList

List of submodules.

{ mkSubList, mkStr, mkPort, mkInt, ... }:
{
  modules.lb.options = {
    backends = mkSubList {
      host = mkStr null;
      port = mkPort 80;
      weight = mkInt 1;
    };
  };
}

# Usage:
nodes.lb.lb.backends = [
  { host = "10.0.0.1"; port = 8080; }
  { host = "10.0.0.2"; port = 8080; weight = 2; }
];

Enable Option

mkEnable

Shorthand for lib.mkEnableOption. Used inside modules for sub-features.

{ mkEnable, ... }:
{
  modules.monitoring.options = {
    metrics = mkEnable "Prometheus metrics";
    logging = mkEnable "structured logging";
  };
}

Note: Don’t use mkEnable for the module’s main enable option - nixy adds that automatically.

Multi-platform Setup

Configure NixOS, Darwin, and Home Manager in one flake.

flake.nix

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    nix-darwin.url = "github:LnL7/nix-darwin";
    home-manager.url = "github:nix-community/home-manager";
    nixy.url = "github:anialic/nixy";
  };

  outputs = { nixpkgs, nixy, ... }@inputs: nixy.mkFlake {
    inherit nixpkgs;
    imports = [ ./. ];
    args = { inherit inputs; };
  };
}

targets.nix

{ inputs, nixpkgs, ... }:
{
  targets.darwin = {
    instantiate = { system, modules, specialArgs }:
      inputs.nix-darwin.lib.darwinSystem { inherit system modules specialArgs; };
    output = "darwinConfigurations";
  };

  targets.home = {
    instantiate = { system, modules, specialArgs }:
      inputs.home-manager.lib.homeManagerConfiguration {
        pkgs = nixpkgs.legacyPackages.${system};
        modules = modules;
        extraSpecialArgs = specialArgs;
      };
    output = "homeConfigurations";
  };
}

Platform-specific Modules

# modules/base.nix - NixOS
{ mkStr, ... }:
{
  modules.base = {
    target = "nixos";
    options.hostName = mkStr null;
    module = { node, ... }: {
      networking.hostName = node.base.hostName;
    };
  };
}

# modules/darwin.nix
{ mkStr, ... }:
{
  modules.darwin = {
    target = "darwin";
    options.hostName = mkStr null;
    module = { node, ... }: {
      networking.hostName = node.darwin.hostName;
      system.stateVersion = 5;
    };
  };
}

# modules/home.nix
{ mkStr, ... }:
{
  modules.home = {
    target = "home";
    options.username = mkStr null;
    module = { node, ... }: {
      home.username = node.home.username;
      home.stateVersion = "24.11";
      programs.home-manager.enable = true;
    };
  };
}

Nodes

# nodes/nodes.nix
{ inputs, ... }:
{
  nodes.workstation = {
    system = "x86_64-linux";
    base.enable = true;
    base.hostName = "workstation";
  };

  nodes.macbook = {
    system = "aarch64-darwin";
    darwin.enable = true;
    darwin.hostName = "macbook";
  };

  nodes."alice-home" = {
    system = "x86_64-linux";
    target = "home";
    home.enable = true;
    home.username = "alice";
  };
}

Deploy-rs Integration

Remote deployment with deploy-rs.

flake.nix

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    deploy-rs.url = "github:serokell/deploy-rs";
    nixy.url = "github:anialic/nixy";
  };

  outputs = { nixpkgs, nixy, ... }@inputs: nixy.mkFlake {
    inherit nixpkgs;
    imports = [ ./. ];
    args = { inherit inputs; };
  };
}

modules/deploy.nix

{ mkStr, ... }:
{
  modules.deploy = {
    options = {
      hostname = mkStr null;
      sshUser = mkStr "root";
    };
  };
}

custom.nix

{ lib, config, inputs, ... }:
let
  deployNodes = lib.filterAttrs (_: n: n.deploy.enable or false) config.nodes;
in {
  flake.deploy.nodes = lib.mapAttrs (name: node:
    let cfg = config.flake.nixosConfigurations.${name};
    in {
      hostname = node.deploy.hostname;
      sshUser = node.deploy.sshUser;
      profiles.system = {
        user = "root";
        path = inputs.deploy-rs.lib.${node._system}.activate.nixos cfg;
      };
    }
  ) deployNodes;
}

nodes/server.nix

{
  nodes.server = {
    system = "x86_64-linux";
    base.enable = true;
    base.hostName = "server";
    deploy.enable = true;
    deploy.hostname = "192.168.1.100";
    deploy.sshUser = "deploy";
  };
}

Deployment

nix run github:serokell/deploy-rs -- .#server