Skip to main content

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.

Source facts

Repository
srid/emanote
Last source activity
May 25, 2026 at 16:18
Detected SKILL.md language
English
Stars
963
Forks
80

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
nix-for-dev
description
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.
# 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
View on GitHub