Skip to content

Docs V2: new information architecture (CIP-3307)#37

Draft
coderdan wants to merge 79 commits into
mainfrom
v2
Draft

Docs V2: new information architecture (CIP-3307)#37
coderdan wants to merge 79 commits into
mainfrom
v2

Conversation

@coderdan

@coderdan coderdan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Long-lived integration branch for the docs V2 overhaul, tracked in CIP-3307. Opened as a draft — it merges only when the migration completes (CIP-3335); section work stacks as PRs targeting this branch, and this PR's Vercel preview is the staging site for the whole overhaul.

What's in the scaffold (CIP-3325)

  • IA.md — living migration checklist at the repo root: one checkbox per planned page, ticked as sections land, with the branch's working rules (how to move a page, redirect flag, URL conventions)
  • New v2docs collection (content/docs) served from the site root (/docs/get-started/…, /docs/integrations/supabase, …) via a required catch-all route. The legacy tree still serves at /docs/stack/* — both trees coexist until every section migrates, then the legacy tree is deleted
  • Frontmatter facet model: Diátaxis type, components, audience, integration (category / setup / pairsWith), and review-tracking fields (verifiedAgainst, reviewBy)
  • All 8 sections scaffolded with nav + stubs, including /docs/integrations/supabase (the Supabase listing's link target)
  • v2-redirects.mjs — full legacy→v2 map (85/85 pages covered), gated behind ENABLE_V2_REDIRECTS=1 so previews serve both trees during migration; the flag flips on at merge. bun run validate-redirects is wired into prebuild: a legacy page without a mapping fails CI
  • /docs/quickstart vanity redirect (ungated — no legacy traffic on that path)
  • llms.txt / llms-full.txt / sitemap / .mdx raw-markdown mirror all cover the v2 tree (listed first)
  • Removed the AI-citation redirect /reference/eql → /stack/reference/eql — its source collided with (and shadowed) the v2 page at that path

How to review the preview

  • New tree: /docs/get-started, /docs/integrations/supabase, /docs/security/compliance, /docs/reference/eql (stubs for now)
  • Legacy tree unchanged: /docs/stack/quickstart etc.
  • Agent surface: append .mdx to any v2 page URL; /docs/llms.txt

Merge checklist (CIP-3335, end of migration)

  • All IA.md sections ticked; every redirect destination resolves
  • ENABLE_V2_REDIRECTS=1 set for production
  • content/stack, /stack routes, and the legacy loader deleted
  • Final consistency sweep + Supabase listing revision

https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P

coderdan added 2 commits July 2, 2026 17:17
Foundation for the docs overhaul tracked in CIP-3307:

- IA.md: living migration checklist at the repo root (one checkbox per
  planned page; sections tick off as they land on this branch)
- New `v2docs` collection (content/docs) served from the site root via a
  required catch-all route, alongside the legacy tree (content/stack)
  at /stack until every section migrates
- Frontmatter facet model: Diátaxis `type`, `components`, `audience`,
  `integration` (category / setup / pairsWith), and review-tracking
  fields (`verifiedAgainst`, `reviewBy`)
- Section scaffold: get-started, integrations (incl. the
  /integrations/supabase stub the Supabase listing links to), concepts,
  compare, guides, security, solutions, reference — meta.json + stubs
- v2-redirects.mjs: full legacy→v2 map (85/85 pages covered), gated
  behind ENABLE_V2_REDIRECTS=1 so the preview serves both trees during
  migration; /quickstart vanity redirect ships ungated
- scripts/validate-v2-redirects.ts wired into prebuild: CI fails if a
  legacy page has no v2 mapping
- llms.txt, llms-full.txt, sitemap, and the .mdx raw-markdown mirror
  now cover both trees

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
…rence/eql

Two issues caught by smoke-testing the scaffold:
- Stub descriptions containing ":" broke YAML frontmatter parsing
  (500s across the v2 tree) — descriptions are now quoted.
- The AI-citation redirect "/reference/eql" → "/stack/reference/eql"
  shadowed the v2 /reference/eql page (redirects run before the
  filesystem); removed since the v2 page now serves that path.

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
public-docs Ready Ready Preview, Comment Jul 9, 2026 12:12pm

Request Review

…t bare root

Two fixes from preview review:
- The bare domain root (Vercel preview URLs) 404'd because the app lives
  under the /docs basePath — added a basePath:false redirect / → /docs.
  In production only /docs/* reaches this app, so previews-only.
- The /docs landing was a standalone (home) page disconnected from the
  v2 nav, with every link pointing at legacy /stack URLs. It's now
  content/docs/index.mdx rendered inside DocsLayout (sidebar + search),
  linking the v2 sections. The catch-all became optional ([[...slug]])
  and the (home) route group is deleted (recoverable from history;
  CIP-3327 refines the landing content).
- The landing's raw-markdown mirror serves at /docs/index.mdx (its URL
  is "/", which can't carry the .mdx suffix).

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
Listing "index" explicitly in meta.json pages forced each section's
index out as a separate child item with the same title as its folder
(clicking "Get started" opened a sub-nav containing another
"Get started"). With index unlisted, Fumadocs merges it into the folder
row: the folder itself links to the page, and children are only real
sub-pages (Integrations → Supabase, not Integrations → Integrations).
The root meta.json keeps "index" — at tree root there is no folder row
to merge into, so the landing needs its own sidebar item.

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
Folders whose only page is their index still rendered as collapsible
sidebar folders with a chevron pointing at nothing. getV2PageTree()
now collapses such folders into plain page items (recursively, so
guides/* and reference/* leaves flatten too); a section becomes a
folder again automatically when its first real sub-page lands.

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
MDX links (markdown and Card hrefs) render through the Link component,
which prefixes the /docs basePath — hardcoded /docs/... links rendered
as /docs/docs/... and 404'd. Convention (enforce via CIP-3349 lint):
internal links in content are always basePath-relative.

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR scaffolds the “Docs V2” information architecture by introducing a new content/docs tree served from the site root (under the existing /docs basePath), while keeping the legacy content/stack tree available at /docs/stack during migration. It also adds a gated legacy→v2 redirect map plus CI validation to ensure legacy pages won’t become orphaned when redirects are enabled.

Changes:

  • Add a new v2docs collection + v2source loader, plus root catch-all routes/layout for the v2 docs tree.
  • Introduce a full legacy→v2 redirect map (v2-redirects.mjs) gated by ENABLE_V2_REDIRECTS=1, with a prebuild validation script to enforce coverage.
  • Scaffold v2 section stubs/meta and update sitemap + LLM surfaces (llms.txt, llms-full.txt, .mdx raw mirror) to include the v2 tree first.

Reviewed changes

Copilot reviewed 65 out of 65 changed files in this pull request and generated 25 comments.

Show a summary per file
File Description
v2-redirects.mjs Adds the full legacy /stack/* → v2 redirect mapping list (flag-gated in Next config).
next.config.mjs Wires in gated v2 redirects, adds preview root redirect, and adds .mdx rewrite for v2 raw mirror; removes old /reference/eql redirect collision.
scripts/validate-v2-redirects.ts Adds CI/prebuild gate to ensure every legacy content/stack page is covered by v2 redirects.
package.json Adds validate-redirects script and runs it during prebuild.
source.config.ts Defines the new v2docs collection and its frontmatter facet schema.
src/lib/source.ts Adds v2source, v2 page tree shaping (flattenEmptyFolders), and broadens getLLMText typing for both trees.
src/app/[[...slug]]/layout.tsx Adds the v2 docs layout (DocsLayout + v2 tree) at the root catch-all segment.
src/app/[[...slug]]/page.tsx Adds the v2 docs page renderer, metadata generation, and markdown mirror URL generation.
src/app/llms.mdx/v2/[[...slug]]/route.ts Adds the v2 raw-markdown mirror route for agent/LLM consumption.
src/app/llms.txt/route.ts Lists v2 pages first, then legacy pages, in llms.txt.
src/app/llms-full.txt/route.ts Emits the concatenated markdown for v2 pages first, then legacy pages.
src/app/sitemap.ts Includes both v2 and legacy pages in sitemap (v2 first).
src/app/og/docs/[...slug]/route.tsx Import ordering tweak only.
src/app/api/search/route.ts Import ordering tweak only.
src/app/layout.tsx Import ordering tweak only.
src/app/stack/layout.tsx Import ordering tweak only.
src/app/stack/[[...slug]]/page.tsx Import ordering tweak only.
src/proxy.ts Import ordering tweak only.
src/lib/posthog/provider.tsx Import ordering tweak only.
src/components/icons/supabase.tsx Fixes missing semicolon in return statement.
src/app/(home)/page.tsx Removes the standalone legacy docs landing page implementation.
src/app/(home)/layout.tsx Removes the legacy home layout wrapper.
IA.md Adds the migration checklist + branch workflow rules for the v2 overhaul.
content/docs/meta.json Adds v2 root meta defining top-level section ordering.
content/docs/index.mdx Adds the v2 docs landing page content (Cards + LLM surface links).
content/docs/get-started/meta.json Adds v2 “Get started” section meta.
content/docs/get-started/index.mdx Adds v2 “Get started” stub page.
content/docs/integrations/meta.json Adds v2 “Integrations” section meta.
content/docs/integrations/index.mdx Adds v2 “Integrations” stub page.
content/docs/integrations/supabase/meta.json Adds v2 Supabase integration meta (custom icon).
content/docs/integrations/supabase/index.mdx Adds v2 Supabase stub page with facet example.
content/docs/concepts/meta.json Adds v2 “Concepts” section meta.
content/docs/concepts/index.mdx Adds v2 “Concepts” stub page.
content/docs/compare/meta.json Adds v2 “Comparisons” section meta.
content/docs/compare/index.mdx Adds v2 “Comparisons” stub page.
content/docs/guides/meta.json Adds v2 “Guides” section meta.
content/docs/guides/index.mdx Adds v2 “Guides” stub page.
content/docs/guides/development/meta.json Adds v2 “Development” guides meta.
content/docs/guides/development/index.mdx Adds v2 “Development” stub page.
content/docs/guides/migration/meta.json Adds v2 “Data migration” guides meta.
content/docs/guides/migration/index.mdx Adds v2 “Data migration” stub page.
content/docs/guides/deployment/meta.json Adds v2 “Deployment” guides meta.
content/docs/guides/deployment/index.mdx Adds v2 “Deployment” stub page.
content/docs/guides/troubleshooting/meta.json Adds v2 “Troubleshooting” guides meta.
content/docs/guides/troubleshooting/index.mdx Adds v2 “Troubleshooting” stub page.
content/docs/security/meta.json Adds v2 “Architecture & security” section meta.
content/docs/security/index.mdx Adds v2 “Architecture & security” stub page.
content/docs/security/compliance/meta.json Adds v2 “Compliance” meta under security.
content/docs/security/compliance/index.mdx Adds v2 “Compliance” stub page.
content/docs/solutions/meta.json Adds v2 “Solutions” section meta.
content/docs/solutions/index.mdx Adds v2 “Solutions” stub page.
content/docs/reference/meta.json Adds v2 “Reference” section meta.
content/docs/reference/index.mdx Adds v2 “Reference” stub page.
content/docs/reference/eql/meta.json Adds v2 “EQL” reference meta.
content/docs/reference/eql/index.mdx Adds v2 “EQL” stub page.
content/docs/reference/stack/meta.json Adds v2 “Stack SDK” reference meta.
content/docs/reference/stack/index.mdx Adds v2 “Stack SDK” stub page.
content/docs/reference/proxy/meta.json Adds v2 “Proxy” reference meta.
content/docs/reference/proxy/index.mdx Adds v2 “Proxy” stub page.
content/docs/reference/cli/meta.json Adds v2 “CLI” reference meta.
content/docs/reference/cli/index.mdx Adds v2 “CLI” stub page.
content/docs/reference/auth/meta.json Adds v2 “Auth” reference meta.
content/docs/reference/auth/index.mdx Adds v2 “Auth” stub page.
content/docs/reference/workspace/meta.json Adds v2 “Workspace & account” reference meta.
content/docs/reference/workspace/index.mdx Adds v2 “Workspace & account” stub page.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread content/docs/index.mdx Outdated
Comment thread content/docs/index.mdx Outdated
Comment on lines +26 to +30
<Card title="Integrations" href="/docs/integrations" description="Platforms, ORMs, frameworks, auth providers, and runtimes." />
<Card title="Concepts" href="/docs/concepts" description="How searchable encryption, key management, and identity-aware encryption work." />
<Card title="Guides" href="/docs/guides" description="Development workflow, data migration, deployment, and troubleshooting." />
<Card title="Architecture & security" href="/docs/security" description="Trust model, components, availability, audit, and compliance — for security review." />
<Card title="Solutions" href="/docs/solutions" description="PII protection, HIPAA, AI/RAG, data residency, and provable access." />
Comment thread content/docs/index.mdx Outdated
<Card title="Guides" href="/docs/guides" description="Development workflow, data migration, deployment, and troubleshooting." />
<Card title="Architecture & security" href="/docs/security" description="Trust model, components, availability, audit, and compliance — for security review." />
<Card title="Solutions" href="/docs/solutions" description="PII protection, HIPAA, AI/RAG, data residency, and provable access." />
<Card title="Reference" href="/docs/reference" description="EQL, the Stack SDK, Auth, the CLI, and Proxy — precise API documentation." />
Comment thread content/docs/index.mdx Outdated
Comment thread IA.md Outdated
Comment on lines +20 to +21
- **Moving a page = ** move the file into `content/docs`, update its facets,
fix inbound links, confirm its `v2-redirects.mjs` entry, tick it here.
Comment thread content/docs/guides/index.mdx Outdated
Comment thread content/docs/guides/development/index.mdx Outdated
Comment thread content/docs/guides/deployment/index.mdx Outdated
Comment thread content/docs/guides/migration/index.mdx Outdated
Comment thread content/docs/guides/troubleshooting/index.mdx Outdated
Seven pages replacing the v2-era EQL reference, written against the
eql_v3 branch of cipherstash/encrypt-query-language (3.0.0):

- index: what EQL is, the v3 domain-variant model, install (single SQL
  script, idempotent), dbdev, Docker, migration/runtime permission
  split, managed-Postgres rationale
- types: 10 scalar families × variants matrix; bool storage-only;
  _ord/_ord_ore twins; index terms per variant
- operators: per-variant support matrix, typed-operand rule, no-LIKE,
  fail-loud blockers, query shapes, function-form equivalents
- indexes: functional indexes on term extractors, engagement
  requirements, sort-key form for index-streamed ORDER BY, EXPLAIN
  checklist, large-table build guidance
- json: ste_vec model, per-node-type terms (hm XOR oc), containment +
  GIN, field access, path queries, blocked native jsonb operators
- functions: comparisons, extractors, min/max only (no SUM/AVG),
  version()
- payload-format: v/i/c envelope (wire version still v:2), hm/ob/bf
  term keys, sv document shape, annotated examples (absorbs the legacy
  CipherCell page)

Cross-page consistency verified against the shipped SQL: equality on
_ord variants compares ORE terms (no hm in _ord payloads), and bare
ORDER BY is correct but extractor-form sort keys stream from the index.

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
Comment thread v2-redirects.mjs Outdated
{
source: "/stack/quickstart",
destination: "/get-started/quickstart",
permanent: true,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't make these permanent for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d0ccd7e — all 71 entries flipped to permanent: false, with a header note to revisit as part of CIP-3335 once the map has soaked post-merge.

coderdan added 7 commits July 2, 2026 19:56
EQL is an abstraction over SQL the way Tailwind is over CSS — the docs
now follow the same shape: Install → Core concepts → type categories →
Indexes → query patterns, increasing in complexity. Each type-category
page is the complete reference for its types (variants, payload shape,
operators/functions, example queries on one page).

- index: trimmed to the Install page
- core-concepts (new): the canonical home for shared mechanics —
  variant model, payload anatomy (v/i/c envelope + hm/ob/bf terms,
  absorbs payload-format/CipherCell), typed-operand rule, fail-loud
  blockers, ORE-equality on _ord, term-leakage pointer
- numbers-and-dates, text, booleans (new) + json (reworked): category
  pages; text owns the no-LIKE treatment; json absorbs the sv payload
  shape; booleans framed as "every type has a storage-only variant —
  for bool it's the only one"
- filtering, sorting, grouping-and-aggregates, joins (new): cross-type
  query patterns; joins headlines the same-keyset constraint
- deleted: types.mdx, operators.mdx, functions.mdx, payload-format.mdx
  (content redistributed; URLs never shipped publicly, no redirect debt)
- Anti-drift rule recorded in IA.md: mechanics live ONLY in
  core-concepts; category/query pages link, never restate
- meta.json: flat URLs with ---Types---/---Indexes---/---Queries---
  sidebar separators; legacy redirect map retargeted (queries →
  filtering, cipher-cell → core-concepts)

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
…load v:3

Review feedback on the EQL section:

- Variant tables: generic form first, then full enumeration of every
  concrete domain name (Tailwind-style); capability column made
  concise; "index term carried" column dropped — term internals live
  in core-concepts' payload anatomy
- SEM specifiers documented as a concept in core-concepts: a trailing
  mechanism suffix (_ord_ore) pins WHICH searchable-encryption
  mechanism implements a capability; _ord tracks the default
  (currently ORE). Replaces the "twins" framing. Each orderable type
  page lists its specifiers under an "SEM specifiers" heading, noting
  the OPE specifier arriving for all orderable types (incl. text) in
  the v3 release
- Payload `v` field documented as the EQL version (3) per team
  decision 2026-07-02; all payload examples updated from v:2

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
…perators and Functions

Review feedback:
- Dates & times split out of Numbers — same traits, distinct semantics;
  each page's examples now match its domain (payroll vs audit-event
  time windows / retention cutoffs / newest-first)
- CREATE TABLE examples get an explicit "Example" sub-heading + lead-in
- Operators and Functions are separate sections on every type page —
  operators as the per-variant support matrix, functions as the
  form-equivalents table (+ MIN/MAX, which only exist as functions)
- IA.md: split reflected; query-performance follow-up added (CIP-3351 —
  the v3 branch already folded the v2 perf guide into
  database-indexes.md, which our indexes page absorbed)

Claude-Session: https://claude.ai/code/session_01ACPpFPHvKtrV48nbEYuv7P
- indexes.mdx: cast query-shape example params to their EQL domain
  types, consistent with the typed-operand rule
- numbers/dates-and-times/text: the fail-loud note now scopes to
  operators — ORDER BY on a variant without an ordering term doesn't
  raise, it silently returns a meaningless order (links Sorting)
- v2-redirects.mjs: all entries permanent: false while the IA settles
  (per review); flip to permanent post-merge soak (CIP-3335)
- IA.md: fix unmatched bold around 'Moving a page'
- add placeholder pages for /concepts/searchable-encryption (CIP-3333)
  and /guides/troubleshooting/query-performance (CIP-3351) so the EQL
  reference's forward links resolve instead of 404ing
@coderdan

coderdan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Review comments addressed in d0ccd7e:

  • /docs/-prefixed links (Copilot, ~20 comments): already fixed in 8514932 (fix(v2): make all MDX links basePath-relative) — those comments predate it. CIP-3349 adds the lint to prevent regressions.
  • Redirects: all entries now permanent: false; flip tracked under CIP-3335.
  • IA.md: unmatched-bold fix.
  • Added placeholder pages for /concepts/searchable-encryption (CIP-3333) and /guides/troubleshooting/query-performance (CIP-3351) so forward links from the EQL reference resolve.

Docs V2: EQL v3 reference section, Tailwind-shaped (CIP-3326)
docs(solutions): port the use-cases section into /solutions
coderdan added 3 commits July 9, 2026 16:26
Brings the identity-API fixes (#57, #59) and the EQL docs build fix (#58)
into the v2 branch. Without this, merging v2 to main at launch would revert
them: v2's copy of content/stack still taught LockContext.identify(), which
is deprecated and no longer affects encryption.

One conflict, in scripts/generate-eql-docs.ts. Both branches carry the same
`<tt>`-stripping fix; they differ only in whether the .replace chain is
wrapped in parentheses. Kept v2's formatting.
A page fixed on one branch gets silently reintroduced by a port from the
other. That is how `LockContext.identify()` survived being fixed on main and
came back through a comparison-page port (#43), and how v2's legacy tree kept
teaching it after main was fixed (#60).

`validate-content` scans content/docs and content/stack for API that is
deprecated (LockContext.identify), never existed (withLockContext with an
identityToken option), or was removed in EQL 3.0.0 (eql_v2_encrypted,
add_search_config, the cs_*_v1 config functions), plus stale `protect` and
`@cipherstash/cli` branding.

Rules are scoped: EQL v3 rules apply only to content/docs, since the legacy
tree documents EQL v2 by design. Generated output is skipped; those examples
come from the SDK's own doc comments and get fixed upstream.

A line is exempt when it also says the API is retired, so deprecation
callouts and migration notes can still name the old API in order to warn
readers off it. Naming a dead symbol without saying it is dead is the bug.

Verified against both real regressions: it flags PR #43's original aws-kms
snippet and main's pre-#57 `identityToken` form, and passes on the current
tree.
chore(ci): gate the build on deprecated and non-existent API in docs
coderdan added 9 commits July 9, 2026 16:48
The docs described ZeroKMS three ways, and each description was a different
layer of the same mechanism with the others omitted. Reconciled per product
review:

- Proxy symmetric re-encryption (patent pending) produces the key seed
  server-side. HMAC-based key derivation expands that seed into a unique data
  key per value, client-side. Both are true; neither alone is the story.
- An AWS KMS root key protects the authority key at rest. The authority key is
  the server half of the dual-party split. No per-value data key is ever
  wrapped or stored, so both the "AWS KMS backed" and "no wrapped-key storage"
  claims hold once the layers are separated.
- "No caching needed" was too broad. Proxy caches keyset-scoped ciphers
  (cipher_cache_size, cipher_cache_ttl_seconds). No data key is ever cached,
  stored, or transmitted. The page states both, in a table.

Standardises on AES-256-GCM-SIV. Leaves the index-term leakage model to
/concepts/searchable-encryption, per the anti-drift rule, and links to it.
…Overview

- /security/architecture -> /security/cryptography (H1, nav, slug). Updated
  the v2-redirects.mjs destination for /stack/reference/security-architecture
  and ticked the item in IA.md.

- The section index nav item read "Architecture & security", duplicating the
  folder it sits under. Fumadocs takes the sidebar label and the H1 from the
  same `title`, so add a `navTitle` frontmatter field that overrides the
  sidebar label only, applied in getV2PageTree. The URL and H1 are untouched.
  Reusable for the other section indexes, which have the same duplication.
The key-derivation diagram was ASCII in an unlabelled code fence: monospace,
~80 columns, overflowing the content column.

Adds Mermaid, authored as a ```mermaid fence rather than a component call.
The fence survives the markdown pipeline untouched; the Shiki transformer
copies its source onto the <pre> as data-mermaid, and TrackedCodeBlock swaps
in the diagram at render time.

That ordering is deliberate. fumadocs-mdx serializes the same mdast tree it
renders, so a remark plugin rewriting the fence into <Mermaid chart="..." />
also rewrites the markdown we serve at .mdx and in llms.txt, where a diagram
should degrade to readable source, not an opaque component call with escaped
entities. Shiki ships a mermaid grammar, so the fence is a valid code block
everywhere it isn't a picture.

Mermaid is ~1MB and is imported dynamically inside the effect, so only pages
with a diagram load it and it never enters the server bundle. The component
re-renders on theme toggle, since Mermaid bakes colours into the SVG.
Also gates the build on diagram validity.

Mermaid parses in the browser, and the component catches errors so a bad
chart can't blank the page. That makes the failure mode silent: a diagram
that renders as nothing looks exactly like a diagram nobody noticed was
missing. `validate-mermaid` parses every ```mermaid fence at build time
(mermaid needs a DOM, so jsdom provides one) and fails with the file and
line. Verified by planting a broken diagram.

The component now falls back to rendering the diagram source rather than
nothing, for the render step the parse gate can't cover.

The two deployment diagrams gain what ASCII couldn't express: the key-seed
exchange is bidirectional, and a crossed edge shows that EU ciphertext
cannot be decrypted by the APAC app. Region identifiers now carry the .aws
suffix, matching the regions table.
The diagram and prose had the client key flowing into ZeroKMS as a
"derivation input", which inverts the property the architecture exists to
provide.

ZeroKMS re-encrypts under the authority key alone and returns a key seed. It
never receives the client key, and the request carries no key material. The
client key processes the seed in your infrastructure, and an HMAC-based KDF
expands the result into a per-value data key.

Only the seed crosses the boundary, and it crosses inward. Nothing crosses
outward: not the client key, not the processed seed, not the data key.

Fixes the layer 1/2 prose, the diagram, the trust-model bullets, and both
data-flow paths. Also tightens data-residency, where "both must cooperate"
implied the halves meet somewhere, and where client keys were said to never
leave the region rather than never leave your infrastructure.
The diagram had the key seed arriving from nowhere. The application requests
it by sending a key ID, which is what gives each data key a unique identity
and lets the same seed be requested again to decrypt the value.

Outbound across the boundary: a key ID, which is not key material. Inbound:
the key seed. Nothing else.
Brings the ZeroKMS key-exchange corrections (#63) into the v2 branch. Without
this, merging v2 to main at launch would revert them: v2's copy of
content/stack still told security reviewers that the client key is sent to
ZeroKMS.

Merged cleanly, no conflicts. Second sync in a day; see the tracking issue for
picking a policy rather than doing this reactively.
chore: merge main into v2 (recover the ZeroKMS key-exchange fixes)
docs(security): write the canonical ZeroKMS cryptography page
coderdan added 7 commits July 9, 2026 18:33
The live quickstart contains six errors, not the two on the ticket. Each was
checked against the shipped stash 0.17.1 and @cipherstash/stack 0.19.0, not
against the prose:

1. `cs_match_v1(email) = $1` — no such function in any EQL version.
   Now `WHERE email = $1::public.text_eq`, with the typed-operand rule
   explained, since an untyped operand silently skips the encrypted operator.
2. `import { users } from "./encryption/schema"` — init scaffolds
   `src/encryption/index.ts` (33 references in the CLI package), and exports
   both `users` and `encryptionClient` from it.
3. `decrypt(row.email, { column, table })` — decrypt takes one argument.
4. `.freeTextSearch() // WHERE email LIKE '%alice%'` — LIKE and ILIKE are
   unsupported on all six text variants in EQL v3.
5. "Each column gets its own encryption key" — the key is per value.
6. "Every value gets its own key, bound to an identity" — identity binding
   needs OidcFederationStrategy plus a lock context, and is not what this page
   sets up.

Also drops "The index terms are themselves encrypted, they reveal nothing about
the plaintext", which contradicts the leakage tables on the security and FHE
pages. Replaced with the true, narrower claim: an equality term reveals which
rows share a value.

Restructured as a Diátaxis tutorial with Steps, adds the missing CREATE TABLE
step (the old page never showed the encrypted column type), and links each
mechanism to its canonical page.
stash 0.17.1 does support EQL v3; it is not the default. Verified against
`npx stash@0.17.1 manifest --json`:

  eql install / eql upgrade
    --eql-version <2|3>   EQL generation to target. [default: 2]

`stash init` runs `eql install` as part of its EQL phase (the CLI's own help
says so) with no way to pass the flag, so init lands EQL v2. `eql install` is
idempotent and skips when EQL is present, so switching afterwards needs
--force.

Adds a step: `npx stash eql install --eql-version 3 --force`, then
`stash eql status` to confirm. Notes that the default becomes 3 in a future
release and the step then goes away, and that v3 is a direct install so
--drizzle, --migration and --latest do not apply to it.

Defers to /reference/eql for the full option set rather than restating it.
IA.md assigns /get-started no content: the section is four pages plus the
component hubs, and the mental model belongs to what-is-cipherstash. But the
landing page links to /get-started, so it needs a real page rather than a
dead URL. Written as a link-only router: start with the Quickstart, then
branch by intent (integrating, no app changes, understanding, security
review). It owns no mechanics. navTitle: Overview keeps the sidebar from
repeating the folder name.

Landing page fixes:
- The Quickstart card pointed at /stack/quickstart and the Agent skills card
  at /stack/reference/agent-skills. Both have v2 homes now.
- Dropped "bound to an identity" from the opening line. Identity binding needs
  OidcFederationStrategy plus a lock context; it is not what you get by
  default. Same overclaim removed from the quickstart.
- Removed em-dashes.

Only links pages that exist. what-is-cipherstash, choose-your-stack, examples,
and the hubs get added to the router as they land.
The EQL docs generator fetches the latest release from GitHub with no pin.
eql-3.0.0-alpha.4 removed `eql_v3.ore_cllw`, so the STRICT drift check has
been failing on v2 for every PR, not just this one.

alpha.4 replaces it with an OPE path, and the rename is not cosmetic:

  eql_v3.ore_cllw          -> eql_v3.ord_ope_term   (public extractor)
  payload term key `oc`    -> `op`
  (backing domain)         -> eql_v3_internal.ope_cllw, private

json.mdx and sorting.mdx are updated to match. core-concepts gains an `op`
row in the payload term table, alongside `hm`, `ob`, and `bf`.

The SEM specifier section said OPE was a future addition. It has landed:
`public.<T>_ord_ope` domains exist for every orderable scalar type. The
section now explains why the two mechanisms differ in what they demand of the
database rather than in capability. An OPE term is a bytea domain that orders
under Postgres's default btree operator class, so a functional index on it
works anywhere you can CREATE INDEX. An ORE term needs a custom operator class
and family, which managed platforms frequently block.

Adds a callout that the mechanism behind the unpinned `_ord` is changing to
OPE before 3.0.0 stabilizes, with ORE remaining available pinned as
`_ord_ore`. Documents what alpha.4 ships: `ord_term(integer_ord)` still
returns ore_block_256 today, so the default has not moved yet.
Two follow-ups to the ope_cllw rename.

The drift check only validates schema-qualified symbols, so it never saw the
payload term key. json.mdx still called it `oc` in eight places: the payload
table, both example payloads, the node-type table, and the exclusivity prose.
alpha.4's manifest has no `oc`, `ocf`, or `ocv` anywhere; sv entries carry
`hm` or `op`. Confirmed against eq_term(jsonb_entry), whose own description
reads "whichever deterministic term the sv entry carries — hm (HMAC-256) or op
(CLLW OPE)". grouping-and-aggregates.mdx also still credited MIN/MAX on JSON
leaves to a "CLLW ORE term".

And the cause: generate-eql-docs.ts fetched whatever GitHub released most
recently, and didn't filter prereleases. alpha.3 and alpha.4 shipped a day
apart, so an upstream publish turned this build red with no commit here. The
release is now pinned to eql-3.0.0-alpha.4, overridable for a one-off check
with EQL_RELEASE_TAG. Upgrading EQL becomes a deliberate commit whose drift
the check reports in review.

Prerelease manifests report `"version": "DEV"`, which the EqlVersion banner
rendered as "validated against EQL DEV" on every reference page. When the
manifest omits a real version, the pinned tag supplies one, so the banner now
reads 3.0.0-alpha.4.
fix(eql): document the CLLW OPE ordering term, and pin the EQL release
coderdan added 5 commits July 9, 2026 21:11
Completes the section. Both are ports with the claims checked rather than
carried across.

what-is-cipherstash: mental model, a mermaid components map, the audience
router, and the threat table. Drops three unverifiable performance claims
("100x faster than AWS KMS", "100,000x faster than FHE", "< 1ms query
overhead") because the corpus disagrees with itself: fhe.mdx says 410,000x,
and the pending benchmarks page says 14x for ZeroKMS vs AWS KMS. Numbers
belong on /reference/benchmarks, sourced once.

Also drops "identity and policy baked into every key", which overstates the
default: identity binding needs OidcFederationStrategy plus a lock context.

Adds what the source never said: the terms stored alongside ciphertext are the
price of a WHERE clause, and an equality term reveals which rows share a value.
And a "what it does not protect against" row, because a page that only lists
wins is not a threat model.

choose-your-stack: merges planning-guide and discovery-session into four
ordered decisions (SDK or Proxy, platform, ORM, identity provider), with the
at-rest / in-transit / in-use table and the dev-to-production credential path.
Notes that SDK and Proxy produce the same ciphertext, so the choice is not a
one-way door.

The section index now routes to all three pages and no longer restates the
component table that what-is-cipherstash owns.
Per review: the 100x AWS KMS figure describes unreleased work, so the page
uses 14x, which the benches repo supports and which proxy/index.mdx already
states. The FHE and sub-millisecond claims are defensible and come back.

Each number now names its source rather than floating free:

- Sub-millisecond query overhead, explained (operators inline into functional
  indexes, so Postgres does a normal index scan).
- 410,000x vs FHE, linked to the open tfhe-ore-bench harness. The old page
  said 100,000x; fhe.mdx and searchable-encryption.mdx both say 410,000x, so
  the outlier was the page being ported.
- Up to 14x the throughput of AWS KMS, with the reason (bulk key derivation).

Also frames the trade rather than leading with speed: encryption in use is
slow if you do it with FHE, and searchable encryption buys its speed with
bounded, published leakage.
"The trade" read as an apology. Each scheme's leakage is bounded and
published, which makes it something you choose per column rather than
something that happens to you.

Renamed to "Choosing what each column reveals", with a table mapping declared
capability to what an observer could infer, from "nothing" upward. The point
is now that the goal is not to eliminate leakage but to pick capabilities
whose leakage your threat model already tolerates, and to know exactly what
you picked. A column whose frequency distribution is itself the secret gets
encrypted without that capability and filtered after decryption.

Same reframing on the quickstart's closing bullet, which had the same tone.
- Platform table, Neon/RDS/Aurora/Cloud SQL row: drop "Nothing special."
- Section 4: "Identity binding, if you need it" -> "Identity binding".

Removing "if you need it" changes the framing from optional to standard, so
"Most teams start without this" went with it. Replaced by the reason it
matters: identity binding is what shrinks the blast radius of a compromised
application process, which is the point raised on the threat-model review.
docs(get-started): build the Get started section
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants