Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 11:39
· 839 commits to main since this release
7b9a690

Be your own indexer. One static binary, one command, a live indexed API in under two minutes - no subgraph to author, no Postgres to run, no third-party data dependency, ever.

This release moves data on disk. It ships the command that does it, nuthatch migrate, which moves files and rewrites a config and never re-indexes. On a real two-nest deployment: 0.259s. Read Upgrading to 2.0 before you run it.

What 2.0 means

1.0 promised stability. 2.0 spends the one budget that promise allows - a major - on the things that could not be done compatibly, and on nothing else.

Tenancy moved into the runtime, and the roost is gone. Not renamed: gone. A roost was a container you had to choose before you knew which shape you wanted. There is now one command, nuthatch dev, and the directory decides: a nuthatch.toml runs that one nest, a mounts.toml runs every nest it mounts. A single nest is N=1, not a special case.

A nest is addressed by what it is, not by what you called it. Data is keyed by a content address (its NID) and a mount is (tenant, NID). Two tenants mounting the same nest share one dataset: one backfill, one store, two routes. It is never indexed twice, and removing one mount decrements a reference rather than deleting data someone else is using.

A tenant is an opaque string. nuthatch refcounts it and knows nothing else - no identity, no authn, no quotas, no metering. That stays the gateway's job.

An edit usually costs nothing now

A nest's identity is a hash of its authored inputs, so any edit changes it. Without more, every edit would re-index the chain. Two things stop that:

Early cutoff. A cosmetic edit - a comment, a renamed view, a doc change - changes what the nest is but not what it stores, so the new identity adopts the existing dataset instead of backfilling. On a 428 MB nest: 0.144s, because it moves nothing.

Shared segments. Sealed segments are content-addressed, so two nests decoding the same contract produce byte-identical files and hold one copy between them. A second nest indexing a contract you already index costs no new bytes.

And what can never be reused says so at load rather than leaving you to wonder why edits stay slow:

! view stamped can never be reused across an edit: it calls the volatile function `now()`,
  whose value changes between runs

Bound what a public endpoint will answer

/sql is the product for a local developer and an open analytical query engine for a public one. Since 2.0 that is a per-mount decision: sql = "open" (default), "deny", or "allowlist" - where only declared, named, parameterised queries answer.

The client sends a name and typed arguments, never SQL. Parameters are int or address only, because both have a total validating parse into a form with no escaping hazard - safe by construction rather than by careful escaping. Because this is mount config rather than manifest, changing it re-indexes nothing, and two tenants can share one dataset while exposing different surfaces.

Also in 2.0

[[templates]].events - a factory template can now say which of its ABI's events to decode, instead of the vendored ABI being the only filter. A full UniswapV2Pair ABI decodes six events where a nest usually wants one: not wrong, but a different workload, with nothing to say so. Empty or absent still decodes everything, and a name the ABI does not define is refused at load rather than surfacing as an empty table after the backfill. --from-subgraph now carries the manifest's own eventHandlers across, so an imported nest decodes what the subgraph decoded rather than a superset of it. Both from @PaulieB14.

The N-1 resync tax

There is no upgrade command in 2.0. nest diff and nest upgrade are gone: they carried real information, but only if you remembered to run them. The runtime now classifies at the moment a nest's identity actually changes, and a breaking change is named and refused with nothing moved - --allow-breaking when consumers are ready, or mount the new version under a different alias and move them on their own clock.

Breaking changes

Every one of these is a row in the stability contract, which was published before this release rather than alongside it.

Change Do this
roost.tomlmounts.toml, [roost][runtime] nuthatch migrate
nuthatch roost devnuthatch dev the directory decides what runs
nuthatch nest diff / nest upgrade removed the runtime reports at mount time
on-disk nests/<name>/data/<nid>/, shared segments/ nuthatch migrate
GET /nests roster field roostruntime rename in any scraper
the Starlark config front-end (unreachable since 2026-07-21) nothing
the compose stack no longer ships credentials export POSTGRES_PASSWORD=… and NUTHATCH_CONTROL_TOKEN=… before docker compose up

If you run the compose stack, read that last row. It shipped POSTGRES_PASSWORD: nuthatch and a dev-token-change-me fallback, which defeated the binary's own guard in the one case it exists for: CONTROL_BIND is parameterised, so reaching the control plane from another host means setting it to 0.0.0.0 - at which point a baked-in token means the bind is accepted, protected by a string published in a public repo. Compose now refuses to start and names what to set. Thanks to @PaulieB14 for finding and fixing it.

Additive, so nothing to do: provenance now carries nid, naming which dataset answered rather than only how it decoded - which matters once early cutoff lets a result legitimately come from data a different identity produced.

The stability contract now covers the 2.x line: a minor is 2.1, 2.2; breaking changes wait for 3.0.

Upgrading

nuthatch migrate --dir <copy> --dry-run   # rehearse against a copy; changes nothing
# stop the service, swap the binary
nuthatch migrate --dir <runtime>

The dry run prints the whole plan including every breaking change by name. A plan you have read once is worth more than a command you have memorised.