Determinate Nix: Wasm in Expressions and Flake Schemas

Authors

K19G

grok-bot

Updated

October 31, 2026

Determinate Nix: Wasm in Expressions and Flake Schemas

Goal: Use two Determinate Nix features that ordinary Nix lacks (or lacks in usable form): builtins.wasm to call WebAssembly from the evaluator, and flake schemas so nix flake show / nix flake check understand your custom outputs. Neither replaces overlays, flakes, or modules — they extend evaluation and flake UX. Keep this chapter next to flake layout and checks; do not confuse it with the light overlays chapter.

Version note: the book tracks NixOS 26.05 (“Yarara”) and Nix 2.35+ (re-verified 2026-10-09: 26.05 remains the current stable branch). builtins.wasm ships in Determinate Nix ≥ 3.17.0 behind extra-experimental-features = wasm-builtin. Flake schemas are documented in the Determinate Nix 3.23.1 reference manual; stock Nix understands a built-in schema set, while Determinate adds the extensible schemas / exportedSchemas protocol. This box does not run Determinate Nix, so listings below are checked against upstream docs and the public examples — run them on a Determinate host to confirm.

Mental model

┌─ Nix expression ─────────────────────────────────────────────┐
│  builtins.wasm { path = ./fn.wasm; function = "fib"; } 33    │
│         │                                                    │
│         ▼  Wasmtime (pure host interface; fresh instance)    │
│      fib.wasm  ──►  Nix value (int / list / attrset / …)     │
└──────────────────────────────────────────────────────────────┘

┌─ flake outputs ──────────────────────────────────────────────┐
│  packages / devShells / nixosConfigurations / YOUR OUTPUTS   │
│         │                                                    │
│         ▼  schemas.<outputName> = { version = 1; inventory…} │
│  nix flake show | check   enumerates & validates generically │
└──────────────────────────────────────────────────────────────┘
Piece What it means
builtins.wasm Primop: load a .wasm module, call one export, pass one Nix value in, get one Nix value out
wasm-builtin Experimental feature flag required before the primop exists
Fresh Wasm instance per call No ambient mutable state across calls — preserves evaluation purity
schemas output Attrset of schema defs for this flake’s outputs
inventory Function: flake output → tree of nodes (children / leaf what / evalChecks / …)
roles Declare which CLI verbs apply (nix-build, nix-run, …) without hard-coding output names
Built-in fallback If a flake has no schemas, Nix uses Determinate’s default schema set for known outputs

When to reach for Wasm. Parsing formats Nix lacks (YAML), heavy pure computation, or tree walks that would otherwise force IFD. Prefer a real derivation when the work belongs in the build plan; prefer Wasm when you need the answer during evaluation without IFD.

When to reach for schemas. Custom flake outputs (myTeam.services, lab topologies, internal bundlers) that nix flake show should list and nix flake check should validate — without teaching every tool a new special case.

Worked examples

Case 1: Enable and probe builtins.wasm

On a machine with Determinate Nix 3.17+:

# /etc/nix/nix.custom.conf  (or equivalent Determinate drop-in)
# extra-experimental-features = wasm-builtin
nix eval --extra-experimental-features wasm-builtin --expr 'builtins.wasm'
«primop wasm»

Call shape:

builtins.wasm {
  path = ./nix_wasm_plugin_fib.wasm;  # or builtins.fetchurl / flake `file` input
  function = "fib";
} 33
# → 5702887

Public examples live in DeterminateSystems/nix-wasm-rust (Fibonacci, Mandelbrot, YAML, …). A documented smoke test:

nix build github:DeterminateSystems/nix-wasm-rust
nix eval --extra-experimental-features wasm-builtin --impure --raw --expr \
  'builtins.wasm { path = ./result/nix_wasm_plugin_mandelbrot.wasm; function = "mandelbrot"; } { width = 60; }'

Performance intuition from Determinate’s measurements: naive fib 40 in Nix ~tens of seconds and multi-GB RSS; the Wasm port returns in well under a second with tens of MB. Per-call overhead is still higher than a native Nix function call — batch work inside one Wasm invocation.

Case 2: YAML without IFD (sketch)

Rust side (via nix-wasm-rust host crate) exports fromYAML; Nix side becomes:

# yaml.nix — evaluation-time YAML → Nix values (requires wasm-builtin)
{ wasmModule }:
let
  fromYAML = text:
    builtins.wasm {
      path = wasmModule;
      function = "fromYAML";
    } text;
in
fromYAML ''
  service:
    replicas: 3
    image: ghcr.io/example/app:1.2.3
''

Compared to IFD (runCommand + builtins.fromJSON), this keeps nix flake show / eval-only CI from spawning builders just to read config. Compared to a C++ plugin, the .wasm can live in-tree or as a file flake input — flakes stay self-contained.

Case 3: A minimal custom flake schema

From the Determinate 3.23.1 manual — teach nix flake show about nixosConfigurations-shaped output without special-casing in the CLI:

# flake.nix — schemas output (excerpt)
{
  outputs = { self, ... }: {
    nixosConfigurations.edge = { /* … */ };

    schemas.nixosConfigurations = {
      version = 1;
      doc = ''
        The `nixosConfigurations` flake output defines NixOS system configurations.
      '';
      inventory = output: {
        children = builtins.mapAttrs (configName: machine: {
          what = "NixOS configuration";
          derivationAttrPath = [ "config" "system" "build" "toplevel" ];
        }) output;
      };
    };
  };
}

Leaf nodes may also set evalChecks (attrset of booleans for nix flake check), shortDescription, forSystems, isLegacy, and isFlakeCheck. Non-leaves nest via children.

Case 4: Roles — teach nix build about a custom output

schemas.labImages = {
  version = 1;
  doc = "Disk images for the teaching lab.";
  roles.nix-build = { };   # nix build / nix shell / nix develop may consume these
  inventory = output: {
    children = builtins.mapAttrs (_: drv: {
      what = "lab disk image";
      derivationAttrPath = [ ];  # leaf is the derivation itself
    }) output;
  };
};

Stock roles include nix-build, nix-run, nix-develop, nix-fmt, nix-bundler, nix-search, nix-template. Tools may define more (for example a future nixos-configuration role instead of hard-coding nixosConfigurations).

Case 5: Exporting schemas for other flakes

Determinate’s later schema revision distinguishes schemas you consume locally from schemas you export for others to import (exportedSchemas). Prefer documenting shared schemas in one flake and importing them, rather than copy-pasting inventory functions into every repo. Check the current flake schemas protocol and the “interface change” blog post before pinning a team convention — the attribute names moved once already.

The trap

  1. Expecting builtins.wasm on upstream Nix. As of this writing it is Determinate-first (with an upstream PR in flight). Flakes that require it are not portable to stock nix without a feature flag and binary that provide the primop.
  2. Using Wasm for impure work. No RNG, no ambient FS writes: that is the point. If you punch host functions that read the ambient machine, you break evaluation reproducibility.
  3. Chatty per-item Wasm calls. Instance setup dominates tiny calls. Parse a whole file once; do not call Wasm once per list element in a tight eval loop.
  4. IFD-building the .wasm on every eval. Fetch or vendor the module; compiling it via IFD reintroduces the cost Wasm was meant to avoid.
  5. Schemas that lie. An inventory that always returns evalChecks = { ok = true; } makes nix flake check green while hiding broken outputs. Put real predicates in evalChecks.
  6. Confusing schemas with overlays. Overlays rewrite pkgs. Schemas only describe flake outputs for tooling. Different layer (see the overlays chapter for the fixed-point story).

The boring rule

On Determinate Nix: enable wasm-builtin only when evaluation-time pure compute beats IFD; vendor or file-input the .wasm; keep flakes that must stay upstream-Nix-portable free of builtins.wasm. Add a schemas (and, when sharing, exportedSchemas) entry for every custom output you expect humans and CI to show/check, with an honest inventory.

Try this

  1. On a Determinate host, nix eval --extra-experimental-features wasm-builtin --expr 'builtins.wasm' and run the Mandelbrot example from nix-wasm-rust.
  2. Add a schemas stub for one custom output in a lab flake; compare nix flake show before and after.
  3. Time an IFD YAML parse vs a Wasm fromYAML on a multi-thousand-line file; keep the winner, delete the loser.

Sources