Skip to content

Building Indexes

This guide covers building your own nxv index from nixpkgs channel-release snapshots. This is an advanced topic for users who want to:

  • Build indexes with different date ranges or channels
  • Self-host their own nxv infrastructure
  • Contribute to the official index

Most Users

If you just want to use nxv, run nxv sync to download the pre-built index. A full rebuild from scratch streams ~15 GB of snapshots and takes a few hours.

How It Works

The indexer does not walk nixpkgs git history and does not need a nixpkgs checkout. It ingests channel-release snapshots from releases.nixos.org:

  • 2020-06 onward: every channel release ships a packages.json.br (~380 MB decompressed) that enumerates all ~144k attributes — including nested package sets (python3Packages.*, haskellPackages.*, nodePackages.*, ...) — with versions and metadata. No Nix evaluation is needed for this era.
  • 2020-03-27 → 2020-06: packages.json.br first appears on 2020-03-27, but the boundary falls mid-release-name (20.09pre), so the source is settled by probing each release rather than inferred from its date. Ordinary runs cover this window — a probe costs one request and no evaluation.
  • 2016-09 → 2020-03-27: releases predate packages.json entirely. Opt in with --backfill-evals to evaluate each release's nixexprs.tar.xz with nix-env (the only path that requires nix).

Every stored commit is a real Hydra-built channel commit: the (attribute, version) pair was verifiably present at both ends of its range.

Prerequisites

Software Requirements

  • nxv with indexer feature - The indexer is feature-gated to keep the main binary small
  • Nix - Only for --backfill-evals (pre-2020 era) and --head-eval; the packages.json era needs no Nix at all
  • Network access to releases.nixos.org

No git, no nixpkgs clone.

Hardware Requirements

ResourceFull rebuild
RAM~2 GB (default 4 parallel workers)
Disk~5 GB free (the raw database is ~2 GB)
Network~15 GB streamed (snapshots are parsed in flight, never hit disk)
TimeA few hours for 2020→present; +1.5-3 h with --backfill-evals

Incremental runs (the normal case) take seconds to minutes — a typical 6-hour window sees 0-2 new releases.

Getting the Indexer

bash
# Using Nix flakes (recommended)
nix run github:utensils/nxv#nxv-indexer -- --help

# From source
cargo build --release --features indexer
./target/release/nxv index --help

Indexing Workflow

Full Index (First Time)

bash
nxv index

This ingests every release of the default channels: nixpkgs-unstable (the historical spine, back to 2016) and nixos-unstable-small (the currency channel, typically hours behind master).

To also cover the pre-2020 era (requires nix):

bash
nxv index --backfill-evals

Interrupting with Ctrl+C is safe: each release commits atomically together with its row in the releases ledger, so unfinished releases simply stay pending.

Reading the Pending Count

nxv stats and the end-of-run report split pending releases into the ones a run will pick up and the ones it structurally cannot:

nixos-unstable-small: 4017 ingested, 3099 pending, 1 skipped (newest: …); 2902 pre-2020, needs --backfill-evals
nixpkgs-unstable: 4191 ingested, 22 failed, 1 skipped (newest: …); 22 pre-2020, needs --backfill-evals

The trailing clause counts pending and failed releases from the nix-env era. Neither is a backlog and neither shrinks on its own — no scheduled run includes them, and --retry-failed alone does not reach them (it takes --retry-failed --backfill-evals). It is a standing choice to trade a decade of coarse historical coverage against hours of nix-env evaluation. Everything outside that clause is genuine queued work the next run will attempt.

Resuming and Incremental Updates

Just run the same command again — the releases ledger tracks what has already been ingested, and only new (or still-pending) releases are processed:

bash
nxv index

To re-queue every known release:

bash
nxv index --full

To retry releases that previously failed:

bash
nxv index --retry-failed

Indexing a Specific Date Range

To ingest only releases in a date window:

bash
# Only 2024 onwards
nxv index --since 2024-01-01

# Specific range
nxv index --since 2023-01-01 --until 2024-01-01

Data-Quality Gates

Monitors check each snapshot before anything is written: attribute count floors, rolling baselines, sentinel packages (firefox, thunderbird, nh, python3Packages.requests), birth/death anomalies, and head lag. With --strict, warnings become fatal (this is what CI uses). --report report.json writes the end-of-run coverage report.

Staying Current During Channel Stalls

When the channels lag behind nixpkgs master, --head-eval evaluates master HEAD directly from a GitHub tarball (requires nix):

bash
nxv index --strict --head-eval --report report.json

Publishing

Generate compressed artifacts for distribution:

bash
# Basic publish
nxv publish --output ./publish

# With signing (recommended)
nxv keygen  # Creates nxv.key and nxv.pub
nxv publish --output ./publish --sign --secret-key ./nxv.key

Generated Artifacts

FileSizeDescription
index.db.zst~220 MBZstd-compressed SQLite database
bloom.bin~330 KBBloom filter for fast lookups
manifest.json~1 KBMetadata with checksums
manifest.json.minisig~1 KBminisign signature (when --sign)

Hosting

Upload artifacts to any HTTP server. Update your manifest's url_prefix:

bash
nxv publish --output ./publish \
  --url-prefix "https://example.com/nxv" \
  --sign --secret-key ./nxv.key

Users can then configure their nxv to use your index:

bash
export NXV_MANIFEST_URL="https://example.com/nxv/manifest.json"
export NXV_PUBLIC_KEY="RWTxxxxxxxx..."
nxv sync

Architecture Deep Dive

Pipeline

Each run is: plan → parallel fetch/parse → in-order gating → aggregated upserts → finish.

  1. Plan: list the channel's release directories on the releases.nixos.org S3 bucket and diff against the releases ledger; new directories become pending rows.
  2. Fetch/parse: workers stream each packages.json.br, decompressing brotli and parsing JSON in flight — the ~380 MB document is never materialized in memory or on disk.
  3. Gate: monitors validate each snapshot in release order before any write.
  4. Upsert: widen-only — an existing (attribute_path, version) row only ever has its first_commit_* moved earlier or its last_commit_* moved later. Each release commits atomically with its ledger row.
  5. Finish: rebuild the bloom filter, rebuild FTS (bulk runs drop the triggers and rebuild from scratch), and update watermarks.

Range Semantics

A row means "this (attribute, version) pair was observed at first_commit and at last_commit" — both real Hydra-built channel commits. Interior presence is interpolated, not guaranteed: a version that lives shorter than one channel advance can be missed entirely.

Interruption Safety

There is no checkpoint file. Ctrl+C triggers a graceful shutdown; releases that didn't finish stay pending in the ledger and the next run picks them up. Failed releases are parked with retry backoff instead of aborting the run.

Database Schema

The index uses SQLite with this schema (version 4):

sql
CREATE TABLE package_versions (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,            -- Short package name, e.g. "python3"
    version TEXT NOT NULL,         -- e.g., "3.11.4"
    first_commit_hash TEXT NOT NULL,  -- Earliest channel commit observed with this version
    first_commit_date INTEGER NOT NULL,  -- Unix timestamp (seconds)
    last_commit_hash TEXT NOT NULL,      -- Latest channel commit observed with this version
    last_commit_date INTEGER NOT NULL,   -- Unix timestamp (seconds)
    attribute_path TEXT NOT NULL,  -- e.g., "python3Packages.requests"
    description TEXT,
    license TEXT,                  -- JSON array
    homepage TEXT,
    maintainers TEXT,              -- JSON array
    platforms TEXT,                -- JSON array
    source_path TEXT,              -- e.g., "pkgs/tools/foo/default.nix"
    known_vulnerabilities TEXT,    -- JSON array of CVEs
    UNIQUE(attribute_path, version),
    CHECK(first_commit_date <= last_commit_date)
);

-- Channel-release ingestion ledger: which snapshots have been ingested,
-- which are pending/failed, and their retry/backoff state
CREATE TABLE releases (
    id INTEGER PRIMARY KEY,
    channel TEXT NOT NULL,
    release_name TEXT NOT NULL,
    commit_hash TEXT NOT NULL,
    commit_count INTEGER,
    release_date INTEGER NOT NULL,
    source TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',  -- pending/ingested/failed/skipped
    attempts INTEGER NOT NULL DEFAULT 0,
    last_attempt_at INTEGER,
    attr_count INTEGER,
    error TEXT,
    ingested_at INTEGER,
    UNIQUE(channel, release_name)
);

-- Full-text search index (auto-synced via triggers)
CREATE VIRTUAL TABLE package_versions_fts
USING fts5(name, description, content=package_versions, content_rowid=id);

-- Cover prefix and prefix+version candidate ranking without fetching full rows
CREATE INDEX idx_packages_search_nocase ON package_versions(
    attribute_path COLLATE NOCASE,
    version COLLATE NOCASE,
    (LENGTH(attribute_path) - LENGTH(REPLACE(attribute_path, '.', ''))),
    last_commit_date DESC,
    first_commit_date DESC
);

-- Metadata
CREATE TABLE meta (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

Key Design: One Row Per Version Range

A given (attribute_path, version) pair is exactly one row, enforced by the UNIQUE(attribute_path, version) constraint. Re-observing a pair only widens its range. nxv dedupe exists to repair databases built by pre-0.1.5 indexer versions that left duplicate rows behind.

Prefix searches first rank compact entries from idx_packages_search_nocase, then fetch at most 5,000 full rows by primary key. Bulk ingestion drops this covering index to avoid write amplification and rebuilds it transactionally. If a run is interrupted, the next writable run repairs it; publishing also repairs the index or refuses to emit a slow artifact.

Insecure Packages

is_insecure is not stored as a column — it's derived at query time from known_vulnerabilities (a non-empty JSON array means the package is flagged insecure). The HTTP API's version-history endpoint surfaces this as a boolean alongside the advisory text in vulnerabilities.

Version history resolves the advisory by package name and version, not by attribute path and not by version string alone:

  • Version string alone would be wrong in the obvious direction — nxv 0.7.1 has nothing to do with zeronet 0.7.1.
  • Attribute path alone would be wrong in the subtle direction. Each (attribute_path, version) row keeps the known_vulnerabilities of its newest observation and is never backfilled, so an advisory added after an attribute stopped shipping a version never reaches that row. emacs 28.2 retired in 2023 holding no advisory while emacs28 28.2 carried the identical build until 2025 and picked up CVE-2024-53920.

Name scoping answers "does this software version have a known advisory". That is a broader question than "will nix refuse to build this attribute at this revision" — for the latter, read known_vulnerabilities off the version itself (/packages/{attr}/versions/{version}/first), which is what the copy-to-clipboard commands use to decide on NIXPKGS_ALLOW_INSECURE.

Troubleshooting

A Release Fails to Ingest

Failed releases don't abort the run — they're parked in the releases ledger with retry backoff. Retry them explicitly:

bash
nxv index --retry-failed

A handful of historical releases are genuinely defective upstream (broken 2018 evaluations, corrupt 16.09/17.03 re-uploads) and will stay parked as skipped — that's expected.

Database Corruption

If the database becomes corrupted after a crash:

bash
# Reset to fresh state
rm ~/.local/share/nxv/index.db  # Linux
rm ~/Library/Application\ Support/nxv/index.db  # macOS

# Re-run indexing (or `nxv sync` to re-download the pre-built index)
nxv index

Pre-2020 Versions Missing

The pre-2020 era is opt-in because it requires nix and takes 1.5-3 hours:

bash
nxv index --backfill-evals

Using a Mirror

Set NXV_RELEASES_URL to point the indexer at an alternate releases.nixos.org S3 endpoint (tests, mirrors).

CLI Reference

For complete command documentation, see the Indexer CLI Reference.

Released under the MIT License.