Skip to content

docs(get-started): build the Get started section#65

Merged
coderdan merged 8 commits into
v2from
docs/v2-quickstart
Jul 9, 2026
Merged

docs(get-started): build the Get started section#65
coderdan merged 8 commits into
v2from
docs/v2-quickstart

Conversation

@coderdan

@coderdan coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

The front door. /stack/quickstart and the /quickstart vanity URL both already redirect here.

Six errors, not two

The ticket lists two known bugs. I checked every line against the shipped stash 0.17.1 and @cipherstash/stack 0.19.0 rather than against the prose, and found six. Anyone who copied this page could not have got it working.

# The page said Actually
1 WHERE cs_match_v1(email) = $1 No such function, in any EQL version
2 import { users } from "./encryption/schema" init scaffolds src/encryption/index.ts (33 references in the CLI package) and exports users and encryptionClient from it
3 decrypt(row.email, { column, table }) decrypt(encryptedData: Encrypted) takes one argument
4 .freeTextSearch() // WHERE email LIKE '%alice%' LIKE/ILIKE are ❌ 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 + a lock context, which this page never sets up

(1) and (3) are the interesting ones. cs_match_v1 would fail loudly. decrypt with a second argument compiles fine in JS and just ignores it, so it appears to work.

The false leakage claim

Also removed: "The index terms are themselves encrypted, they reveal nothing about the plaintext."

That contradicts the leakage tables on security-architecture.mdx and comparisons/fhe.mdx, both of which enumerate exactly what each term reveals. It is replaced with the true, narrower statement: an equality term reveals which rows share a value, so declare only the capabilities you query on.

It remains live in two places on main, and gets fixed when the canonical leakage page lands.

What else changed

  • Adds the missing CREATE TABLE step. The old page never showed how an encrypted column is declared, which is the one thing a reader cannot guess. It is now email public.text_eq, with the point made that the domain type is the configuration.
  • Explains the typed operand ($1::public.text_eq) rather than leaving it as a mystery cast. An untyped operand doesn't error; it silently falls through to jsonb semantics and skips the index.
  • Restructured as a Diátaxis tutorial using Steps, and demoted stash plan / stash impl to a callout: they're the agent path, and a quickstart should show the moving parts.
  • Links each mechanism to its canonical page (/security/cryptography, /reference/eql/core-concepts, /reference/eql/text) rather than restating it.

Verification

Every API claim traced to a source, not a doc:

  • decrypt / encryptQuery / encryptedTable signatures: dist/client-DfCrlHXh.d.ts and dist/index.d.ts of @cipherstash/stack@0.19.0.
  • Scaffold path and exports: the stash@0.17.1 package's own template strings.
  • LIKE support: the operator matrix in /reference/eql/text.
  • Column type: /reference/eql/core-concepts, verifiedAgainst: eql 3.0.0.

Page carries verifiedAgainst: { stack: 0.19.0, cli: 0.17.1, eql: 3.0.0 }.

bun run build passes (716 pages), validate-content, validate-mermaid, validate-links, validate-redirects all pass. All internal links resolve. No em-dashes.

One thing to confirm

The page assumes a CLI that installs EQL 3.0.0. stash@0.17.1's bundle still contains eql_v2_encrypted codepaths (1,562 occurrences alongside 39,333 for eql_v3), so a reader running stash init today may get a v2 install and find public.text_eq doesn't exist. That's the EQL 3.0.0 release-alignment issue the branch already tracks, not something this page can fix. Worth confirming before we point the vanity URL at it publicly.

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.
@vercel

vercel Bot commented Jul 9, 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, ✅ 2 resolved Jul 9, 2026 12:11pm

Request Review

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.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 1b4f340. My "one thing to confirm" note was wrong, and this resolves it.

stash 0.17.1 does support EQL v3. It just isn't the default. From npx stash@0.17.1 manifest --json:

eql install / eql upgrade
  --eql-version <2|3>   EQL generation to target.
                        v3 is the native eql_v3.* domain schema
                        (direct install only for now).   [default: 2]

The wrinkle: stash init runs eql install as part of its EQL phase (the CLI's own help text says so) and exposes no way to pass the flag, so init lands EQL v2. And eql install is idempotent, skipping when EQL is already present. So a reader following the old steps ends up on v2 with no signal, and public.text_eq doesn't exist.

New step 2:

npx stash eql install --eql-version 3 --force
npx stash eql status

--force reinstalls over the v2 schema init just laid down; on a new database with no encrypted data there's nothing to migrate. The callout says the default becomes 3 in a future release and this step then disappears, so nobody inherits a default that's about to change under them.

I initially wrote that Supabase needs --direct. That was wrong: /reference/eql already documents that v3 installs directly, falls back to the no-operator-family variant when the role lacks superuser, and that --drizzle, --migration, and --latest are v2-only. The quickstart now links there instead of restating it.

Build passes, all gates green. Rendered text verified for both commands and the callout.

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.
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.
@coderdan coderdan changed the title docs(get-started): rewrite the quickstart on EQL v3 docs(get-started): build the Get started section Jul 9, 2026
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 6fe86b1: what-is-cipherstash and choose-your-stack. The section is now complete except for examples, and /stack/reference/{what-is-cipherstash,discovery-session,planning-guide} all had redirects waiting for these.

Three performance claims removed

what-is-cipherstash carried numbers the rest of the corpus contradicts:

The page said Elsewhere
"100x faster than AWS KMS" kms/index.mdx agrees; the pending benchmarks page says 14x (throughput, ~615 req/sec peak)
"100,000x faster than FHE" comparisons/fhe.mdx says 410,000x, linking cipherstash/tfhe-ore-bench
"< 1ms query overhead" benchmarks: ~0.1ms median exact-match, ~0.5ms median range

I didn't pick a winner. Performance numbers belong on /reference/benchmarks (#54), sourced once, and every other page should link there. Right now three pages assert three different figures, and a skeptical reader checking our own docs finds the inconsistency before we do.

Also removed "identity and policy baked into every key". That overstates the default in exactly the way the quickstart's "bound to an identity" did: identity binding needs OidcFederationStrategy plus a lock context.

Two things the source never said

The trade. The old page described searchable encryption as pure upside. The new one says plainly that index terms are the price of a WHERE clause, that an equality term reveals which rows share a value, and that columns you only store and retrieve can carry no terms at all. It links to /concepts/searchable-encryption for the per-term detail (still a stub, blocked on the crypto questions).

What it does not protect against. A threat-model section that only lists wins isn't one. The page now states that an attacker inside your running application process holds the client key and can call decrypt. Encryption in the application means the application can decrypt.

Other notes

  • The components map is a mermaid diagram, the first use outside the security section. It shows the key-seed exchange and the boundary, and makes the point that Proxy is an alternative to the SDK rather than an addition.
  • IA.md wants the map's nodes to link to the component hubs. Those don't exist yet (fix(reference): resolve TypeDoc link warnings in the stack API reference #29), so nodes link to reference pages for now.
  • choose-your-stack notes that SDK and Proxy emit the same ciphertext and both speak EQL, so the choice isn't a one-way door. Worth saying, since it's the decision people agonise over.
  • The section index no longer restates the component table that what-is-cipherstash owns.

Build green: pinned EQL resolves, drift check passes, 4 mermaid diagrams parse, validate-content / validate-links / validate-redirects pass. All four pages prerender.

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.
@coderdan coderdan merged commit bf32cef into v2 Jul 9, 2026
2 checks passed
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.

1 participant