- Status: Accepted
- Date: 2026-06-17
- Deciders: Achref Soua
v0.15.0 is the last build-here increment before the v1.0.0 launch. It bundles
the documentation and packaging polish the launch needs with the two
honestly-deferred DCPE follow-ups that have been carried since ADR-0031:
-
A documentation site. Quiver has a deep
docs/tree — ADRs, the index design, the wire protocol, the security docs, the architecture C4 views — but no browsable site that ties them into a reader's journey (concepts → quickstart → self-hosting → features → API/SDKs → security → architecture). The launch DoD (docs/roadmap.md) requires a live docs site. -
Clean-clone quickstart polish. The launch DoD also requires that a fresh
git clonebuilds, runs, and answers a first query "in minutes." The README quickstart must be verified end-to-end against a clean checkout and any drift fixed. Install is from source (cargo install --path crates/quiver-cli, binary namequiver) because the crates.ioquiver-cliname is an unrelated squatted crate and the SDKs are unpublished (ADR/roadmap note,v0.12.0). -
A native TypeScript
DcpeCipher. ADR-0031 shipped DCPE with a Rust reference cipher (quiver_crypto::dcpe) and a native Python port (quiver.dcpe), validated against each other by a cross-language known-answer test (KAT). The TypeScript SDK got only thevector_encryption="dcpe"create flag — there is no native TS cipher, so a JS/TS client cannot actually encrypt vectors or queries. ADR-0031 recorded this as a follow-up. -
The two deferred DCPE Scale-And-Perturb (SAP) hardening steps. ADR-0031 implemented the core SAP construction (scale
s, bounded d-ball perturbationλ,c = s·m + λ, HMAC tag) but honestly deferred the paper's two additional hardening steps (Fuchsbauer, Ghosal, Hauke & O'Neill, "Approximate Distance-Comparison-Preserving Symmetric Encryption," IACR ePrint 2021/1666, SCN 2022; the scheme behind IronCore's Cloaked AI):- a secret component shuffle — a key-derived permutation of the vector's dimensions; and
- plaintext-distribution normalisation — mapping the plaintext distribution to a canonical one before encrypting, so the fixed-radius perturbation gives consistent protection.
The hard constraint shared by 3 and 4: never invent cryptography. Every
primitive must come from an audited library (RustCrypto on the Rust side;
cryptography on the Python side; @stablelib / @noble on the TS side, matching
the optional-peer-dependency pattern vector.ts already uses). The DCPE cipher is
client-side and the server stores DCPE vectors as ordinary L2 vectors, so none of
the cipher work changes the on-disk format or the kill -9 crash gate.
Use mdBook (pure Rust, a single binary, no Node toolchain) rather than
Fumadocs/Nextra. It matches Quiver's frugal, pure-Rust ethos, builds in CI without
node_modules or a JS build step, and keeps the docs site in the same toolchain as
the rest of the repo. The richer React-based options were rejected only for the
Node/pnpm build weight they add to an otherwise dependency-lean project.
The book lives at apps/docs/ (book.toml + src/). To avoid duplicating the
canonical docs/ content, deep chapters reuse the existing files via mdBook's
{{#include}} preprocessor (the security deep-dives, the architecture/index
design, the migration and replication guides, the REST/gRPC and MCP references);
the connective chapters (introduction, concepts, quickstart, self-hosting,
features overview, SDK usage) are authored fresh in src/. A just docs recipe
runs mdbook build; building the site is not added to just verify (the
authoritative gate stays the Rust pipeline, ADR-0015) so the gate keeps no new
dependency, but the book is verified to build locally.
Walk a fresh checkout through build → run → first query, reconcile the README
quickstart and the new quickstart/self-hosting docs pages with what actually
runs, and fix any drift. Keep install from source and the binary name quiver.
Port quiver_crypto::dcpe to sdks/typescript/src/dcpe.ts, exported at the
quiver-client/dcpe subpath (mirroring quiver-client/vector and
quiver-client/encryption) so the core client stays dependency-free. The
primitives — ChaCha20 (the CSPRNG keystream), HKDF-SHA256, HMAC-SHA256, SHA-256 —
come from audited @stablelib packages added as optional peer dependencies,
exactly as @stablelib/xchacha20poly1305 is for vector.ts. A new TS KAT decrypts
a Rust-produced vector (tag verifies bit-exact; plaintext recovered within float
tolerance), modelled on sdks/python/tests/test_dcpe.py::test_kat_matches_the_rust_reference.
The 64-bit keystream words are read with BigInt so the u64 arithmetic
(>> 11, % n) matches Rust/Python exactly; the [0,1) mapping and the
Box-Muller normals then run in IEEE-754 f64 (number), matching within ULPs.
Harden the cipher in all three implementations (Rust reference, Python, the new TS
port) and regenerate the cross-language KAT. DCPE is experimental and pre-1.0, so a
breaking cipher change is acceptable — and there is no on-disk format
change (DCPE vectors are and remain ordinary stored L2 vectors). To make the
break loud rather than silent, the integrity-tag domain is bumped to
quiver/dcpe/v2/tag, so a v1 ciphertext fails a v2 integrity check with a
clear error instead of decrypting to garbage.
The encryption pipeline becomes, applied to a plaintext m ∈ ℝ^d (each step
must preserve the L2 distance-comparison ordering, since the untrusted server
ranks on the ciphertext):
-
Normalise (optional, ordering-preserving). A fixed global affine transform
m₁ = (m − μ) · α, whereμ ∈ ℝ^dis a per-dimension shift vector (default0) andα > 0is a single scalar scale (default1). A uniform per-coordinate shift cancels in any difference (‖(a−μ)−(b−μ)‖ = ‖a−b‖) and a single positive scalar scales every distance by the sameα(‖α(a−b)‖ = α‖a−b‖), so the ordering is preserved exactly and the transform is invertible (m = m₁/α + μ). The transform is fixed at cipher construction — supplied once by the caller from a one-time measurement of their corpus (its per-dimension mean and a global RMS radius) or key-derived — never recomputed per batch. -
Shuffle (key-derived permutation
π). Permute thedcomponents with a permutation derived from the key alone (a new HKDF sub-keyquiver/dcpe/v2/shuffle, a ChaCha20 CSPRNG with a fixed all-zero IV, and a fully-specified Fisher–Yates). L2 distance is invariant under any permutation of coordinates, so applying the sameπto every vector and every query preserves all pairwise distances exactly (no recall loss); the inverse permutation reverses it on decrypt. The permutation depends only on(key, d), so it is identical for data and queries and reproducible across languages. -
Scale-and-perturb (unchanged from ADR-0031).
c = s · π(m₁) + λ, withλthe uniform d-ball perturbation of radius(s/4)·βdrawn from(prf_key, iv). -
Tag (domain bumped). HMAC-SHA256 over
(quiver/dcpe/v2/tag ‖ β ‖ iv ‖ c).
Decryption verifies the tag, re-derives λ, then reverses the pipeline:
m = T⁻¹(π⁻¹((c − λ)/s)).
What the shuffle buys: it hides the axis-alignment of the ciphertext — which
ciphertext coordinate corresponds to which plaintext coordinate — frustrating
coordinate-wise analysis and the simplest embedding-inversion shortcuts, at zero
recall cost (it is an exact isometry). A key-derived permutation is the
shuffle the paper and the prompt specify; a full key-derived orthonormal rotation
would hide more but cannot be made bit-reproducible across languages in f32, so a
permutation is both the specified and the more honest (KAT-exact) choice.
What normalisation buys, and its honest limit. Normalisation makes the
approximation factor β mean the same thing across datasets: the perturbation
radius (s/4)·β is fixed in absolute terms, so unless the data occupies a
canonical scale, the same β over- or under-protects. A global recentre + rescale
canonicalises the cloud's centroid and overall scale (the dominant distributional
signals) while preserving the search ordering.
It cannot do per-axis variance whitening. Standardising each dimension
independently (m_i' = (m_i − μ_i)/σ_i with a per-axis σ_i) is the most
effective normalisation — but a per-axis scaling is an anisotropic transform that
re-weights the dimensions in the L2 distance, so it does not preserve the
distance-comparison ordering the untrusted server relies on. There is therefore a
genuine, unavoidable trade-off: full per-axis whitening is incompatible with
server-side distance-preserving search. We implement the strongest normalisation
that is compatible — a fixed global affine (full per-axis shift + a single global
scale) — and document the limit plainly rather than ship an anisotropic transform
that would silently break recall. This is the limit the design was required to
confront, stated honestly.
ADR-0031 is left immutable (it records the original v1 scheme as shipped);
this ADR records the hardened v2 cipher as the new state.
- No on-disk format change. DCPE vectors are stored as ordinary L2 vectors; the
cipher is entirely client-side. The
kill -9crash gate is untouched by construction (the ADR-0031 stance). - Breaking cipher change, bounded.
v1-encrypted vectors are notv2-compatible. DCPE is experimental and off by default; thev2/tagdomain makes a version mismatch fail closed (an integrity error), and the break is documented in the ADR,docs/security/dcpe.md, and the SDK docs. - No server/wire change. REST/gRPC/MCP already take
vector_encryption="dcpe"and store the ciphertext as an L2 vector; shuffle and normalisation are client-side, so no surface beyond the three ciphers and the KAT changes.
- + A browsable docs site (mdBook) covering the full reader journey, built from the canonical docs with no duplication and no Node toolchain.
- + A verified clean-clone quickstart — a launch-DoD item closed.
- + TypeScript clients can finally use DCPE end-to-end (encrypt vectors and queries), validated bit-for-bit against the Rust reference by a KAT — closing the last DCPE SDK gap.
- + The cipher gains the two published hardening steps to the extent they are
compatible with searchable encryption: axis-alignment hiding at zero recall cost,
and consistent
βsemantics via global normalisation. - − A breaking DCPE cipher change (acceptable: experimental, off by default, no on-disk impact, fail-closed on version mismatch).
- − An honest, documented limitation: DCPE still cannot do per-axis whitening without sacrificing server-side searchability, and remains not semantically secure and L2-only (unchanged from ADR-0031). DCPE composes with, but does not replace, encryption-at-rest or the semantically secure client-side mode (ADR-0032).
- − mdBook is a new (dev-time, optional) build tool; it is deliberately kept
out of
just verifyso the authoritative gate gains no dependency.
- Fumadocs/Nextra for the docs site — richer (search, MDX components) but adds
a Node/pnpm build and
node_modulesto a pure-Rust + minimal-SDK repo; rejected for weight against the frugal ethos. - A key-derived orthonormal rotation instead of a permutation — hides more
(mixes coordinates rather than relabelling them) and is still an L2 isometry, but
f32matrix multiplication is not bit-reproducible across languages, breaking the KAT-exactness guarantee; the prompt and the literature specify a permutation, so a permutation it is. - Per-axis (anisotropic) whitening — the most effective normalisation, but it breaks the L2 distance-comparison ordering and so is incompatible with untrusted-server search; rejected, and the incompatibility is documented as a limit rather than worked around.
- A data-adaptive normalisation recomputed as data arrives — would diverge between the encrypt-time and query-time clients and leak the evolving distribution; rejected in favour of a transform fixed at cipher construction.
- A non-breaking, versioned cipher carrying both
v1andv2— needless complexity for an experimental, off-by-default, pre-1.0 feature with no on-disk footprint; a clean breaking change with a fail-closed domain is simpler and honest.
All four pieces shipped in v0.15.0.
- Docs site.
apps/docsis an mdBook (book.toml+src/) with ajust docsrecipe. The connective chapters (introduction, concepts, quickstart, self-hosting, the feature overviews, SDK usage, the ADR index) are authored insrc/; the deep reference chapters reuse the canonical top-leveldocs/via mdBook{{#include}}(the four security docs, the architecture/index design, and the migration / replication / REST-gRPC / MCP references), so there is a single source of truth. The build stays out ofjust verify(ADR-0015); the builtbook/is git-ignored. - Quickstart polish. The clean-clone build → run → first-query path was verified end to end against a fresh build (CLI surface, default ports 6333/6334, the demo key, and the Python/TypeScript SDK signatures all match; a live boot with encryption-at-rest on created a collection, upserted, and searched through the Python SDK). No drift was found; the README now points into the docs site.
- Native TypeScript
DcpeCipher.sdks/typescript/src/dcpe.ts, exported at thequiver-client/dcpesubpath, ports the cipher using audited@stablelibpackages (chacha,hkdf,hmac,sha256) as optional peer dependencies. The 64-bit keystream words are read withBigIntso theu64arithmetic matches Rust/Python exactly. - DCPE v2 hardening. The Rust reference (
quiver_crypto::dcpe), the Python port (quiver.dcpe), and the new TS cipher all gained: a key-derived Fisher–Yates component shuffle (HKDF sub-keyquiver/dcpe/v2/shuffle, a ChaCha20 CSPRNG with a fixed zero IV) applied identically to every vector and query; an optional ordering-preserving global affineNormalization(a per-dimension shift plus a single positive scalar scale); and the integrity-tag domain bumped toquiver/dcpe/v2/tag. Per-axis variance whitening is not implemented — it is anisotropic and would break the L2 distance-comparison ordering — and that limit is documented in each cipher's module docs and indocs/security/dcpe.md. The cipher is client-side, so there was no server/wire change and no on-disk format change.
- DCPE v2 cross-language KAT. The Rust, Python, and TS ciphers are validated
against a single known-answer vector produced by the Rust reference: the
HMAC-SHA256 tag verifies bit-exact (HKDF + HMAC are deterministic across
languages) and the plaintext is recovered within float tolerance after
un-shuffling — proving all three agree, including the derived permutation
(
[2,6,1,5,7,0,4,3]for the KAT key atd=8). Each language also tests the shuffle (a valid, deterministic, key-dependent permutation; an effect atβ=0), normalisation (round-trip, nearest-neighbour preservation, and rejection of bad parameters / dimension mismatch), and the prior DCPE properties (which still hold, since the shuffle and normalisation are transparent when the same cipher encrypts and queries).just verify,just test-py(50), andjust test-ts(43) are green. - Docs site.
mdbook build apps/docsis clean; the embedded canonical content (the DCPE spec, the index design, migration, the threat model) renders and search is built.
- E. Fuchsbauer, R. Ghosal, N. Hauke, A. O'Neill. Approximate Distance-Comparison-Preserving Symmetric Encryption. IACR ePrint 2021/1666; SCN 2022. (The Scale-And-Perturb scheme; the shuffle and normalisation hardening.)
- IronCore Labs, Cloaked AI — the SAP scheme applied to embedding vectors.
- ADR-0031 (DCPE
v1, immutable), ADR-0032 (semantically secure client-side vectors), ADR-0012 (client-side payload encryption — the optional-peer-dependency SDK pattern), ADR-0015 (CI policy —just verifyis the gate), ADR-0018 (SDK & integration strategy).