diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 0.12.0.1 - 2026-11-05
+
+- **The README in the package describes 0.12: its version bounds, the server's required `CACHE_API_KEY`, and which codecs are pure.** The README in the 0.12.0.0 tarball, the one Hackage shows, gave bounds that exclude 0.12 itself and a server command that exits at once, since the server refuses to start without `CACHE_API_KEY` unless given `--allow-open-writes`. It also called the WAI server optional, where `NovaCache.Server` is always part of the library and only the executable sits behind `-f server`, and counted one-shot xz decompression and zstd compression among the `IO` operations, where both are pure. The test command now passes `--enable-tests`, as CI's does.
+
 ## 0.12.0.0 - 2026-11-01
 
 - **`nova-cache-server reclaim-nars` lists, and with `--delete` removes, the NARs no narinfo references.** A client uploads a path's NAR before its narinfo, as upstream's `BinaryCacheStore::addToStoreCommon` does, so a NAR whose narinfo the server refuses, or whose push dies before the narinfo, stayed under `nar/` for good: the server has no route that deletes, and gains none. The subcommand runs on the host against the store directory. A narinfo names its NAR by its `URL` alone, as upstream reads it (`getFile(info->url)`), with the compression suffix part of the name. Three rules guard a run. A NAR is eligible only once it is two weeks old by its modification time (`--min-age-hours` overrides), because a NAR is unreferenced until its push sends the narinfo and nothing bounds how long a push takes; git's `gc.pruneExpire` gives unreferenced objects the same two weeks against the same race. A narinfo that cannot be read, parsed, or resolved to `nar/<file>` blocks the whole run instead of being skipped, since the NAR it names would otherwise look unreferenced. And a store holding NARs and no narinfo at all blocks it too, since one that lost its `narinfo/` directory, which the server recreates empty when it starts, looks the same as one whose every NAR is unreferenced. A run is not atomic with the server, so it belongs on a cache with no push in progress: a narinfo that arrives during a run for a NAR past the minimum age, as upstream's `nix copy` can send after skipping a NAR already present, loses that NAR. Upstream ships no garbage collection for binary caches, so there is no behavior to match beyond the URL. The library gains `NovaCache.Reclaim` (the pure selection and the store scan) and, in `NovaCache.Store`, `listNarFiles`, `listNarInfoFiles`, `removeNar` and `fileStoreAt`, a constructor that creates no directories; `time` joins the dependencies.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -6,26 +6,28 @@
 
 nova-cache implements the Nix binary cache protocol in Haskell: nix-base32,
 NAR serialization (whole-tree and streaming), narinfo parsing, store paths,
-Ed25519 signing and upload validation, and an optional WAI cache server.
+Ed25519 signing and upload validation, a WAI application for a cache server,
+and an optional server executable.
 Bounded xz, zstd and bzip2 codecs are the public `nova-cache:xz`,
 `nova-cache:zstandard` and `nova-cache:bzip2` sublibraries, and the zstd codec
 also compresses for uploads.
 
-Parsing, formatting, hashing, signing and validation are pure. Reading and
-writing trees, hashing files, the codecs, the store and the server run in
-`IO`. nova-cache is the protocol layer under
+Parsing, formatting, hashing, signing, validation, one-shot xz decompression
+and zstd compression are pure. Reading and writing trees, hashing files, the
+streaming codecs, zstd and bzip2 decompression, the store and the server run
+in `IO`. nova-cache is the protocol layer under
 [nova-nix](https://github.com/Novavero-AI/nova-nix).
 
 ## Installation
 
 ```cabal
-build-depends: nova-cache >= 0.11 && < 0.12
+build-depends: nova-cache >= 0.12 && < 0.13
 ```
 
 The codecs are separate sublibraries, named alongside the main library:
 
 ```cabal
-build-depends: nova-cache:{nova-cache, xz, zstandard} >= 0.11 && < 0.12
+build-depends: nova-cache:{nova-cache, xz, zstandard} >= 0.12 && < 0.13
 ```
 
 libbz2 and libzstd are bundled. liblzma comes from the `xz` package, which
@@ -50,7 +52,8 @@
 -- Parse a narinfo and sign it
 case (parseNarInfo raw, parseSecretKey "mykey:base64...") of
   (Right ni, Right sk) -> print (sign sk ni)  -- Right "mykey:<base64 sig>"
-  _                    -> error "parse failed"
+  (Left err, _)        -> putStrLn err
+  (_, Left err)        -> putStrLn err
 ```
 
 ```haskell
@@ -83,9 +86,12 @@
 ## Server
 
 ```bash
-cabal run --flag server nova-cache-server -- --port 5000 --store ./nix-cache
+CACHE_API_KEY=change-me cabal run --flag server nova-cache-server -- --port 5000 --store ./nix-cache
 ```
 
+Without `CACHE_API_KEY` the server refuses to start. `--allow-open-writes` runs
+it with unauthenticated writes, for local testing only.
+
 The protocol lives in the `NovaCache.Server` library module as a WAI
 `Application`, so the cache can be embedded in another server with its own
 root page. The bundled executable is one such embedding.
@@ -147,11 +153,10 @@
 
 ### Public cache
 
-A public instance runs at `cache.novavero.ai`. It serves store paths that
-nova-nix builds, which today means its Windows MinGW-w64 toolchain seed
-([nova-nix#207](https://github.com/Novavero-AI/nova-nix/issues/207)). To use
-it from Nix, add it to `/etc/nix/nix.conf`. On a multi-user install, Nix
-ignores substituters set by users it does not trust.
+A public instance runs at `cache.novavero.ai`. It serves the closure of
+nova-nix's Windows `hello` recipe, which nova-nix's CI publishes on every push
+to its main branch. To use it from Nix, add it to `/etc/nix/nix.conf`. On a
+multi-user install, Nix ignores substituters set by users it does not trust.
 
 ```
 extra-substituters = https://cache.novavero.ai
@@ -165,7 +170,7 @@
 ```bash
 cabal update
 cabal build all -f server --enable-tests --ghc-options="-Werror"
-cabal test -f server --ghc-options="-Werror"
+cabal test -f server --enable-tests --ghc-options="-Werror"
 ```
 
 ## License
diff --git a/nova-cache.cabal b/nova-cache.cabal
--- a/nova-cache.cabal
+++ b/nova-cache.cabal
@@ -4,7 +4,7 @@
 -- 3.0 an in-package component name shadowed its external namesake
 -- (the hazard the zstandard library's name dodges, documented there).
 name:               nova-cache
-version:            0.12.0.0
+version:            0.12.0.1
 synopsis:           Nix binary cache protocol: NAR, narinfo, signing and a WAI server
 description:
   The Nix binary cache protocol in Haskell: nix-base32, NAR serialization
@@ -84,13 +84,11 @@
     -Wincomplete-record-updates
     -Wincomplete-uni-patterns
 
--- The bounded xz decoder as a solver-visible opt-in: a consumer that
--- substitutes foreign caches writes build-depends: nova-cache:xz and
--- gets the module; everyone else never builds the xz package's
--- bundled liblzma.  This replaces the manual xz flag - a dependency's flag
--- cannot be set from a consumer's .cabal file, which forced mirror
--- flags and matching constraints downstream (nova-nix#27) - while
--- keeping the 0.5.0.0 lesson: xz stays out of the default install.
+-- The bounded xz decoder as a public sublibrary: a consumer that
+-- substitutes from xz caches depends on nova-cache:xz, and nobody else
+-- builds the xz package's bundled liblzma.  A sublibrary rather than a
+-- flag, because a dependency's flag cannot be set from a consumer's
+-- .cabal file.
 library xz
   visibility:       public
   exposed-modules:  NovaCache.Xz
