diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Unreleased
+
+- **`NovaCache.Xz` moves to the public `nova-cache:xz` sublibrary; the `xz` flag is gone.** A dependency's flag cannot be set from a consumer's `.cabal` file, so reaching the decoder forced a mirrored flag plus a matching `constraints: nova-cache +xz` in every downstream - two knobs that had to agree and that the solver could not see. `build-depends: nova-cache:xz` now expresses the need directly, and consumers without it still never build the bundled liblzma, keeping the 0.5.0.0 lesson. Builds that passed `-f xz` drop the flag and add the dependency; the module and its API are unchanged.
+
 ## 0.8.0.0 - 2026-08-20
 
 - **Bounded xz decompression returns: the new `NovaCache.Xz`, behind a manual, off-by-default `xz` flag.** 0.5.0.0 removed xz support for having no consumer; foreign-cache substitution (cache.nixos.org serves `.nar.xz`) is the consumer, and the decoder comes back shaped for untrusted input. The consumer knows the narinfo's declared NarSize before decompressing, so `decompress` takes that bound and fails past it - a small compressed input cannot expand to arbitrary memory ahead of the hash check - and the decoder's own state is capped too (`xzMaxDecoderMemoryBytes`; the dictionary size is an attacker-chosen number read from the stream header, and upstream passes no limit there). `withXzSource` decompresses a chunk source into a chunk source under the same limits, pairing with streaming NAR consumption. Concatenated streams decode as one output, matching upstream's `LZMA_CONCATENATED` decoder. The `lzma-static` dependency bundles liblzma's C sources, so the flag needs no system library on any platform - and it stays off by default anyway: the 0.5.0.0 lesson was a compression dependency nobody asked for in every install.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -54,8 +54,8 @@
 
 -- Stream a tree's NAR and hash it in one pass; the archive never
 -- exists in memory. The parsing side is NovaCache.NAR.Stream, a
--- chunk-fed event machine, and the xz flag adds decompression
--- bounded by a narinfo's declared NarSize.
+-- chunk-fed event machine, and the nova-cache:xz sublibrary adds
+-- decompression bounded by a narinfo's declared NarSize.
 narHash <- withNarSource defaultCaseHack path $ \pull ->
   let go ctx = do
         chunk <- pull
@@ -116,7 +116,7 @@
 cabal test
 ```
 
-Optional flags: `--flag server` builds the cache server; `--flag xz` builds the bounded xz decoder (liblzma is bundled - no system library needed). Requires GHC 9.14+ and cabal-install 3.10+.
+Optional extras: `--flag server` builds the cache server, and the public `nova-cache:xz` sublibrary carries the bounded xz decoder (liblzma is bundled - no system library needed) - consumers depend on it with `build-depends: nova-cache:xz`. Requires GHC 9.14+ and cabal-install 3.10+.
 
 ---
 
diff --git a/nova-cache.cabal b/nova-cache.cabal
--- a/nova-cache.cabal
+++ b/nova-cache.cabal
@@ -1,12 +1,13 @@
 cabal-version:      3.0
 name:               nova-cache
-version:            0.8.0.0
+version:            0.9.0.0
 synopsis:           Pure-first Nix binary cache protocol library
 description:
   A pure-first library implementing the Nix binary cache protocol -
   nix-base32, NAR serialization (whole-tree and streaming), narinfo
-  parsing, Ed25519 signing, store path handling, content validation,
-  and bounded xz decompression - with an optional WAI server.
+  parsing, Ed25519 signing, store path handling, and content
+  validation - with an optional WAI server, and bounded xz
+  decompression as the public @nova-cache:xz@ sublibrary.
 
 license:            Apache-2.0
 license-file:       LICENSE
@@ -29,11 +30,6 @@
   default:     False
   manual:      True
 
-flag xz
-  description: Build NovaCache.Xz, bounded xz decompression (lzma-static bundles liblzma; no system library)
-  default:     False
-  manual:      True
-
 library
   exposed-modules:
     NovaCache.Base32
@@ -63,11 +59,27 @@
     , vector              >= 0.12 && < 0.14
     , wai                 >= 3.2 && < 3.3
 
-
-  if flag(xz)
-    exposed-modules: NovaCache.Xz
-    build-depends:   lzma-static >= 5.2.5 && < 5.3
+  hs-source-dirs:   src
+  default-language:  Haskell2010
+  default-extensions:
+    BangPatterns
+    OverloadedStrings
+  ghc-options:
+    -Wall
+    -Wcompat
+    -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 lzma-static'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.
+library xz
+  visibility:       public
+  exposed-modules:  NovaCache.Xz
   hs-source-dirs:   src
   default-language:  Haskell2010
   default-extensions:
@@ -79,6 +91,11 @@
     -Wincomplete-record-updates
     -Wincomplete-uni-patterns
 
+  build-depends:
+      base                >= 4.22 && < 5
+    , bytestring          >= 0.11 && < 0.13
+    , lzma-static         >= 5.2.5 && < 5.3
+
 executable nova-cache-server
   if !flag(server)
     buildable: False
@@ -124,9 +141,6 @@
     , wai-extra           >= 3.1 && < 3.2
 
 test-suite nova-cache-xz-test
-  if !flag(xz)
-    buildable: False
-
   type:             exitcode-stdio-1.0
   main-is:          XzTest.hs
   hs-source-dirs:   test
@@ -138,7 +152,7 @@
   build-depends:
       base                >= 4.22 && < 5
     , bytestring          >= 0.11 && < 0.13
-    , nova-cache
+    , nova-cache:xz
 
 source-repository head
   type:     git
diff --git a/src/NovaCache/Xz.hs b/src/NovaCache/Xz.hs
--- a/src/NovaCache/Xz.hs
+++ b/src/NovaCache/Xz.hs
@@ -14,13 +14,14 @@
 -- Concatenated streams decode as one output, matching upstream's
 -- @LZMA_CONCATENATED@ decoder in libutil's compression sink.
 --
--- This module builds only under the @xz@ cabal flag.  The
+-- This module lives in the public @nova-cache:xz@ sublibrary.  The
 -- @lzma-static@ dependency bundles liblzma's C sources, so no system
 -- library is needed on any platform - but it is still an extra C
 -- build that consumers without foreign-cache needs should not pay
--- for, and a default-on compression flag broke downstream installs
--- once already (0.5.0.0).  The flag is manual and off by default;
--- consumers that substitute from foreign caches turn it on.
+-- for, and a default-on compression dependency broke downstream
+-- installs once already (0.5.0.0).  Consumers that substitute from
+-- foreign caches depend on @nova-cache:xz@; everyone else never
+-- builds it.
 module NovaCache.Xz
   ( XzLimits (..),
     defaultXzDecoderMemoryBytes,
