diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## 0.2.1.0 — 2026-02-22
+
+- Server: `validateNarInfo` wired into PUT handler — rejects malformed uploads
+  with 400 Bad Request and collected validation errors
+- Server: `Cache-Control: public, max-age=31536000, immutable` on narinfo and
+  NAR GET responses for CDN edge caching
+- Store: default priority changed from 30 to 50 (community cache fallback
+  behind cache.nixos.org at 40)
+- Seed action: fix round-trip validation to account for server-side signing;
+  now verifies StorePath field, signature presence, and NAR fetchability
+- Public binary cache documented with key and nix.conf instructions
+
 ## 0.2.0.0 — 2026-02-22
 
 - New module: `NovaCache.Validate` — pure protocol validation layer
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -232,7 +232,27 @@
 | `PUT` | `/<hash>.narinfo` | Upload narinfo |
 | `PUT` | `/nar/<file>` | Upload NAR file |
 
-### Use as a Nix Substituter
+### Public Binary Cache
+
+A free, public binary cache is available at `cache.novavero.ai`. Add it to
+your Nix configuration:
+
+```nix
+# nix.conf or /etc/nix/nix.conf
+extra-substituters = https://cache.novavero.ai
+extra-trusted-public-keys = cache.novavero.ai-1:2yJK0UZWlDDTpThzEdqfGWaj+j3ljOCGoA50Ims47dM=
+```
+
+Or use it directly:
+
+```bash
+nix build --substituters "https://cache.nixos.org https://cache.novavero.ai" \
+          --trusted-public-keys "cache.nixos.org-1:DLD/YGKmo6OceLp6RsiGCbi5FwMExRzJcoJKanMPe/Q= cache.novavero.ai-1:2yJK0UZWlDDTpThzEdqfGWaj+j3ljOCGoA50Ims47dM="
+```
+
+### Self-Hosted Cache
+
+Run your own instance:
 
 ```bash
 # Push to your cache
diff --git a/exe/Main.hs b/exe/Main.hs
--- a/exe/Main.hs
+++ b/exe/Main.hs
@@ -12,6 +12,7 @@
 import NovaCache.NarInfo (NarInfo (..), parseNarInfo, renderNarInfo)
 import NovaCache.Signing (SecretKey, parseSecretKey, sign)
 import NovaCache.Store (FileStore, getCacheInfo, newFileStore, readNar, readNarInfo, writeNar, writeNarInfo)
+import NovaCache.Validate (validateNarInfo)
 import System.Environment (getArgs, lookupEnv)
 import Text.Read (readMaybe)
 
@@ -119,14 +120,21 @@
         respond (responseLBS HTTP.status200 octetHeaders (BL.fromStrict content))
       Nothing ->
         respond (responseLBS HTTP.status404 textHeaders "not found")
-  -- PUT /<hash>.narinfo (auth required)
+  -- PUT /<hash>.narinfo (auth required, validated)
   ("PUT", [hashNarinfo])
     | Just hashKey <- T.stripSuffix ".narinfo" hashNarinfo ->
         requireAuth cfg req respond $ do
           body <- BL.toStrict <$> strictRequestBody req
-          let signed = signNarInfo (cfgSigningKey cfg) body
-          writeNarInfo (cfgStore cfg) hashKey signed
-          respond (responseLBS HTTP.status200 textHeaders "ok")
+          case parseNarInfo (TE.decodeUtf8 body) of
+            Left err ->
+              respond (badRequest (T.pack err))
+            Right ni -> case validateNarInfo ni of
+              Left errs ->
+                respond (badRequest (T.unlines (map (T.pack . show) errs)))
+              Right _ -> do
+                let signed = signNarInfo (cfgSigningKey cfg) body
+                writeNarInfo (cfgStore cfg) hashKey signed
+                respond (responseLBS HTTP.status200 textHeaders "ok")
   -- PUT /nar/<file> (auth required)
   ("PUT", ["nar", fileName]) ->
     requireAuth cfg req respond $ do
@@ -189,14 +197,29 @@
 boolText True = "1"
 boolText False = "0"
 
+-- | 400 Bad Request with a text error message.
+badRequest :: Text -> Response
+badRequest msg = responseLBS HTTP.status400 textHeaders (BL.fromStrict (TE.encodeUtf8 msg))
+
 -- | Content-Type: text/plain headers.
 textHeaders :: HTTP.ResponseHeaders
 textHeaders = [(HTTP.hContentType, "text/plain")]
 
 -- | Content-Type: application/x-nix-narinfo headers.
+-- Narinfo files are content-addressed (keyed by store path hash) and
+-- immutable once written, so they are safe to cache indefinitely at the
+-- CDN edge.
 narInfoHeaders :: HTTP.ResponseHeaders
-narInfoHeaders = [(HTTP.hContentType, "text/x-nix-narinfo")]
+narInfoHeaders =
+  [ (HTTP.hContentType, "text/x-nix-narinfo"),
+    (HTTP.hCacheControl, "public, max-age=31536000, immutable")
+  ]
 
 -- | Content-Type: application/octet-stream headers.
+-- NAR files are content-addressed (keyed by content hash) and immutable
+-- once written, so they are safe to cache indefinitely at the CDN edge.
 octetHeaders :: HTTP.ResponseHeaders
-octetHeaders = [(HTTP.hContentType, "application/octet-stream")]
+octetHeaders =
+  [ (HTTP.hContentType, "application/octet-stream"),
+    (HTTP.hCacheControl, "public, max-age=31536000, immutable")
+  ]
diff --git a/nova-cache.cabal b/nova-cache.cabal
--- a/nova-cache.cabal
+++ b/nova-cache.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               nova-cache
-version:            0.2.0.0
+version:            0.2.1.0
 synopsis:           Pure Nix binary cache protocol library
 description:
   A focused, minimal, pure-first library implementing the full Nix binary
diff --git a/src/NovaCache/Store.hs b/src/NovaCache/Store.hs
--- a/src/NovaCache/Store.hs
+++ b/src/NovaCache/Store.hs
@@ -53,8 +53,10 @@
 narSubdir = "nar"
 
 -- | Default cache priority (lower = preferred by Nix).
+-- Community caches should use a higher number than cache.nixos.org (40)
+-- so the official cache is preferred and we serve as a fallback.
 defaultPriority :: Int
-defaultPriority = 30
+defaultPriority = 50
 
 -- | Default Nix store directory.
 defaultStoreDir :: Text
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -553,7 +553,7 @@
         removeDirectoryRecursive tmpDir
         ok1 <- assertEqual "storeDir" "/nix/store" storeDir
         ok2 <- assertTrue "wantMassQuery" wantMass
-        ok3 <- assertEqual "priority" 30 priority
+        ok3 <- assertEqual "priority" 50 priority
         pure (ok1 && ok2 && ok3),
       test "sanitizePath rejects traversal" $
         assertEqual "dotdot" Nothing (Store.sanitizePath ".."),
