A Standard Opinionated Nix Project Structure
nix flake
README.md

zenix #

A Standard Opinionated Nix Project Structure inspired by numtide/blueprint.

Usage #

Add zenix to your flake.nix and pass the entire inputs set to mkFlake:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    zenix.url = "git+https://tangled.org/jeffydc.xyz/zenix?shallow=1";
  };

  outputs = inputs: inputs.zenix.lib.mkFlake {
    inherit inputs;
    systems = [ "x86_64-linux" ];
    # Optional: customize the nixpkgs import used for each system.
    mkPkgs = system: import inputs.nixpkgs {
      inherit system;
      config.allowUnfree = true;
    };
  };
}

By default, zenix reads files under nix/ relative to your flake. For example:

nix/
├── apps/hello.nix                           → apps.<system>.hello
├── checks/custom.nix                        → checks.<system>.custom
├── devshells/default.nix                    → devShells.<system>.default
├── formatter.nix                            → formatter.<system>
├── hosts/workstation/configuration.nix      → nixosConfigurations.workstation
├── modules/core/nixos-module.nix            → nixosModules.core
├── modules/core/darwin-module.nix           → darwinModules.core
├── modules/network/firewall/nixos-module.nix → nixosModules.network-firewall
├── overlays/default.nix                     → overlays.default
├── packages/hello.nix                       → packages.<system>.hello
└── templates/simple/flake.nix               → templates.simple

Nested host directories are joined by hyphens: for example, nix/hosts/region/box/configuration.nix becomes nixosConfigurations.region-box (and receives hostname = "region-box"). Host configuration files must be inside a named directory.

For nix-darwin, add a nix-darwin flake input and put host files at nix/hosts/<name>/darwin-configuration.nix. Each module directory may contain nixos-module.nix, darwin-module.nix, or both; each file is exported only to its corresponding output. Module entry points must be inside a named directory. Shared logic can live outside nix/modules/ and be imported by both entry points.

Package-style files (apps, checks, devshells, packages, and formatter.nix) are functions taking zenix arguments first, followed by arguments supplied through pkgs.callPackage. For example, nix/packages/hello.nix can contain:

{ inputs, flake }:
{ pkgs, ... }:
pkgs.hello

Here inputs is the flake input set and flake is inputs.self. Host and module files take the same zenix arguments before the NixOS or nix-darwin module function; hosts also receive hostname. Overlay files take zenix arguments before their final: prev: function. Templates are directories, not Nix functions.

Nested module and overlay paths become names joined by hyphens: for example, modules/network/firewall/nixos-module.nix becomes nixosModules.network-firewall. Avoid collisions such as module directories a/b/ and a-b/, or overlay files a/b.nix and a-b.nix. checks also includes dev shells, packages, and host system closures prefixed with devshell-, package-, nixos-, and darwin- respectively. Check names must be unique; for example, checks/package-hello.nix conflicts with packages/hello.nix.

mkPkgs receives a system name and returns a nixpkgs package set. Omit it to use the default import inputs.nixpkgs { inherit system; }. You can also set root to a different relative directory or a path, and pass systems to choose target platforms. Run nix flake show to inspect the generated outputs.

Development Guide #

  • Use snake_case for internal codes.

  • Hard limit comment length at 80 columns.

  • Always run make fmt and make ci after the final changes.

LICENSE #

MIT