# Nix For Dev

> Use this when setting up Nix for a development project (devShell + package build) and you care about `nix develop` being fast. Covers the zero-inputs flake.nix + npins + default.nix/shell.nix layout, sub-flakes for non-user-facing Nix, and language-specific recommendations.

- Skill: `srid/nix-for-dev` (Agent Skill)
- Install (CLI): `npx skillmds@latest add srid/nix-for-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/srid/nix-for-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: srid (https://skillmd.com/u/srid)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/srid/nix-for-dev

---


# Nix for development

A low-overhead Nix setup for dev projects: ~1s cold `nix develop`, ~0.1s warm, with a clean separation between the user-facing flake and internal Nix. Reference implementation: [juspay/kolu](https://github.com/juspay/kolu).

## Core principle: zero flake inputs

The top-level `flake.nix` declares **no `inputs`** at all. Each flake input adds ~1.5s of fetcher-cache verification on cold eval; a single `nixpkgs` input costs ~7s. With zero inputs, cold `nix develop` is ~1.0s, warm ~0.1s.

Instead, pin sources with [npins](https://github.com/andir/npins) and import them via `fetchTarball` / `callPackage` from files under `nix/`.

```nix
# flake.nix — slim, zero inputs
{
  outputs = { self, ... }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
      eachSystem = f: builtins.listToAttrs (map
        (system: {
          name = system;
          value = f (import ./nix/nixpkgs.nix { inherit system; });
        })
        systems);
    in
    {
      packages = eachSystem (pkgs: {
        default = import ./default.nix { inherit pkgs; };
      });
      devShells = eachSystem (pkgs: {
        default = import ./shell.nix { inherit pkgs; };
      });
    };
}
```

**Do not** add `nixpkgs`, `flake-parts`, `git-hooks`, etc. as flake inputs. If a downstream consumer needs to override the pinned nixpkgs, they can override the npins source (`NPINS_OVERRIDE_nixpkgs=/path`).

## File layout

```
flake.nix          # slim wrapper, zero inputs
default.nix        # main package(s), pkgs ? import ./nix/nixpkgs.nix { }
shell.nix          # devShell,        pkgs ? import ./nix/nixpkgs.nix { }
nix/
  nixpkgs.nix      # imports npins source + applies overlay
  overlay.nix      # injects leaf packages into pkgs
  env.nix          # env vars shared by build + devShell + wrapper
  packages/<name>/ # callPackage-style leaf packages
npins/
  default.nix      # generated by npins (do not edit)
  sources.json     # pinned source revisions
```

`default.nix` and `shell.nix` both accept `pkgs ? import ./nix/nixpkgs.nix { }` so they also work via plain `nix-build` / `nix-shell`, not just `nix develop`.

## npins workflow

```bash
npins init                                   # creates npins/ and pins nixpkgs
npins add github nixos nixpkgs --branch nixpkgs-unstable
npins update                                 # update all pins
npins update nixpkgs                         # update one
```

`nix/nixpkgs.nix`:

```nix
# Pinned nixpkgs import — managed by npins.
# To update: npins update nixpkgs
let
  sources = import ../npins;
  nixpkgs = import sources.nixpkgs;
in
args: nixpkgs (args // {
  overlays = (args.overlays or [ ]) ++ [ (import ./overlay.nix) ];
})
```

## Leaf packages via overlay

Pure callPackage-style packages live in `nix/packages/<name>/default.nix` and are auto-injected via `nix/overlay.nix`:

```nix
# nix/overlay.nix
final: _prev: {
  my-fonts = final.callPackage ./packages/fonts { };
}
```

Packages that need per-invocation arguments (commit hash, build-time env) stay in the top-level `default.nix` — overlays are for things that legitimately belong on `pkgs`.

## Shared env vars

Define a single `nix/env.nix` returning an attrset; both the build derivation and the devShell spread it into their `env`. This prevents drift between `nix build` and `nix develop`.

```nix
# nix/env.nix
{ pkgs }: {
  MY_FONTS_DIR = pkgs.my-fonts;
  MY_GH_BIN    = "${pkgs.gh}/bin/gh";
}
```

## devShell conventions

- Use `pkgs.mkShell` directly. **Do not** introduce `flake-parts` to "structure" it.
- Use `pkgs.writeShellApplication` (not `writeShellScriptBin`) — strict mode + `runtimeInputs` validation. Always set `meta.description`.
- Run `nixpkgs-fmt` from the devShell rather than wiring up `formatter` perSystem.
- Expose extra shells via `overrideAttrs` so the base stays fast:

```nix
devShells = eachSystem (pkgs:
  let default = import ./shell.nix { inherit pkgs; };
  in {
    inherit default;
    e2e = default.overrideAttrs (prev: {
      env = (prev.env or { }) // {
        PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;
      };
    });
  });
```

`nix develop .#e2e` for the heavier shell; the default stays cold-start-fast.

## Sub-flakes for non-user-facing Nix

Module integration tests (home-manager, NixOS, Darwin) genuinely need `flake-parts`-style inputs (`home-manager`, `nix-darwin`). Keep those inputs out of the top-level flake by nesting a sub-flake under `nix/<name>/flake.nix`. CI builds it with `--override-input` pointing back at the parent:

```nix
# nix/home/example/flake.nix
{
  inputs = {
    self_pkg.url     = "github:owner/repo";   # parent; CI passes --override-input
    nixpkgs.url      = "github:nixos/nixpkgs/nixpkgs-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };
  outputs = { nixpkgs, home-manager, self_pkg, ... }: {
    # nixosConfigurations / checks that exercise self_pkg.homeManagerModules.default
  };
}
```

Users running `nix develop` / `nix run` on the top-level flake never evaluate this graph. Only CI does.

## Language templates

### Haskell

Use [haskell-flake](https://github.com/srid/haskell-flake) via its **standalone** entry point `lib.evalHaskellProject` (not the flake-parts module). See [haskell.nixos.asia/standalone](https://haskell.nixos.asia/standalone) for the full API.

In keeping with the zero-inputs principle, pin `haskell-flake` via npins (`npins add github srid haskell-flake`) and call it from `default.nix`:

```nix
# default.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
  sources = import ./npins;
  haskell-flake = import sources.haskell-flake;
  project = (haskell-flake.lib { inherit pkgs; }).evalHaskellProject {
    projectRoot = ./.;
    modules = [{
      settings.mypackage.haddock = false;
      devShell.tools = hp: { inherit (hp) fourmolu; };
    }];
  };
in
project.packages.mypackage.package
```

Wire `project.devShell` into `shell.nix` the same way. If you need flake-parts and `nixos-unified` autowiring (multi-package projects, fully wired checks), see the `nix-haskell` skill — it uses [haskell-template](https://github.com/srid/haskell-template) and trades startup time for ergonomics.

### TypeScript / pnpm

See [`nix-typescript`](../nix-typescript/SKILL.md) for `fetchPnpmDeps` and hash management.

### Dev services

For multi-process dev environments (server + watcher + db), use [`process-compose-flake`](https://github.com/Platonic-Systems/process-compose-flake) and [`services-flake`](https://github.com/juspay/services-flake) via their **standalone** entry points (no flake-parts needed). Both flakes have zero inputs themselves, so pinning them via npins keeps the top-level `flake.nix` zero-input too:

- `process-compose-flake` exposes [`lib.evalModules` / `lib.makeProcessCompose`](https://github.com/Platonic-Systems/process-compose-flake/blob/main/nix/lib.nix) for module evaluation outside flake-parts.
- `services-flake` exposes `processComposeModules.default` (a path) — pass it as a module to `evalModules`.

```nix
# shell.nix
{ pkgs ? import ./nix/nixpkgs.nix { } }:
let
  sources = import ./npins;
  pcLib = import "${sources.process-compose-flake}/nix/lib.nix" { inherit pkgs; };
  servicesMod = pcLib.evalModules {
    modules = [
      "${sources.services-flake}/nix/process-compose"
      { services.redis."r1".enable = true; }
    ];
  };
in
pkgs.mkShell {
  inputsFrom = [ servicesMod.config.services.outputs.devShell ];
}
```

Reference: [`services-flake/example/without-flake-parts`](https://github.com/juspay/services-flake/tree/main/example/without-flake-parts) and [`doc/without-flake-parts.md`](https://github.com/juspay/services-flake/blob/main/doc/without-flake-parts.md).

## Companion docs

- [`nix-perf`](../nix-perf/SKILL.md) — diagnosing slow `nix develop` / `nix flake archive`
- [`nix-justfile`](../nix-justfile/SKILL.md) — justfile recipe conventions for Nix projects
- [`nix-typescript`](../nix-typescript/SKILL.md) — pnpm + Nix conventions
- [`nix-haskell`](../nix-haskell/SKILL.md) — flake-parts + haskell-template variant (when ergonomics > cold start)
- [juspay/kolu](https://github.com/juspay/kolu) — full reference implementation

