diff --git a/.changeset/adapter-readmes-refresh.md b/.changeset/adapter-readmes-refresh.md new file mode 100644 index 000000000..893559a40 --- /dev/null +++ b/.changeset/adapter-readmes-refresh.md @@ -0,0 +1,22 @@ +--- +'@cipherstash/stack-supabase': patch +'@cipherstash/stack-drizzle': patch +'@cipherstash/stack-prisma': patch +--- + +Refresh the adapter READMEs (they ship on each package's npm page): + +- **stack-supabase**: full rewrite — the old README was a stub. Now covers the + introspecting `encryptedSupabase(url, key)` factory, the encrypted filter + surface (`eq`/`neq`/`in`/`match`, range, `order()` on OPE-backed columns), + the EQL 3.0.2 PostgREST limitations, and the quick start. +- **stack-drizzle**: fix the hero example — `ops.contains` is encrypted-JSONB + containment (a `types.Json` column) and would throw on a text column; the + free-text operator is `ops.matches`. The operator table no longer lists + `contains` under free-text match. +- **stack-prisma**: correct the encrypted-column-type catalog (domain-named + factories across text/integer/float/numeric/date/timestamp/boolean/JSON, not + "six types"), fix the authentication docs URL, and replace relative links + (which 404 on npm) with absolute ones. +- All three: add the badge header and the architecture diagram from the root + README. diff --git a/.changeset/stack-readme-single-source.md b/.changeset/stack-readme-single-source.md new file mode 100644 index 000000000..a886ee849 --- /dev/null +++ b/.changeset/stack-readme-single-source.md @@ -0,0 +1,9 @@ +--- +'@cipherstash/stack': patch +--- + +The npm README for `@cipherstash/stack` is now the root Stack README — the +package copy had drifted badly out of date. The root `README.md` is the single +source of truth: the package's `prebuild` script copies it in before every +build (npm cannot publish a symlink, so a real file has to ship in the +tarball), and a unit test fails CI if the two files ever drift apart. diff --git a/README.md b/README.md index 489c7f83a..008155c61 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,389 @@ +
- - CipherStash Logo + + CipherStash

CipherStash Stack for TypeScript

-Built by CipherStash -License -Docs -Join the community on Discord +

Searchable, application-level encryption for building privacy-first apps.

+ npm version + npm downloads + GitHub stars + Docs + Discord + License + +
⭐ Star this repo if encryption you can actually query is your thing!
-## What is the stack? -- [Encryption](https://cipherstash.com/docs/stack/cipherstash/encryption): Field-level encryption for TypeScript apps with searchable encrypted queries, zero-knowledge key management, and first-class ORM support. +## Encryption-level security without the pain + +Field-level encryption is the strongest way to protect data. +But if DB functionality or performance suffers, you need to justify the pain. +Searchable Encryption nukes the trade-off: encryption-level security without the pain. + +* Searchable Encryption for any Postgres including Supabase, RDS, Aurora, Prisma Postgres and Neon +* Works with Supabase.js, Prisma Next, Drizzle or plain SQL +* Built-in key management — automatic rotation, auditing and up to 14x faster than AWS KMS +* Integrates with Supabase Auth, Clerk, Auth0 and Okta + +
+ +## Quick starts + +### Use the wizard + +Takes 5-10 minutes, starts on the **free developer tier** ([sign up][signup]), includes agent handoff. + +```bash +# Run this to start (or just ask Claude to) +npx stash init +``` + +### ORM/database-specific guides + +| Quick start | Guide | +|---|---| +| **Supabase** | [Supabase quickstart →][supabase] | +| **Prisma Next** | [Prisma Next quickstart →][prisma-next] | +| **Drizzle ORM** | [Drizzle quickstart →][drizzle] | +| **Raw PostgreSQL (`pg`)** | [PostgreSQL quickstart →][encryption] | +| **DynamoDB** | [DynamoDB quickstart →][dynamodb] | -## Quick look at the stack in action +> The Stack also ships a `stash` CLI for auth, schema, and database setup. See the [SDK reference][reference]. -**Encryption** +## What's in the Stack + +### 🔐 Searchable encryption + +Encrypt individual fields and still run real queries against them — all on ciphertext, in PostgreSQL: + +| Query type | Operations | Docs | +|---|---|---| +| **Equality** | `=`, `IN` | [Equality queries →][query-equality] | +| **Free-text search** | fuzzy `matches` | [Text search →][query-match] | +| **Range & ordering** | `<`, `>`, `BETWEEN`, `ORDER BY`, `MIN`/`MAX` | [Range queries →][query-range] | +| **Encrypted JSON** | containment (`@>`), JSONPath selectors | [JSON queries →][query-json] | + +With [EQL v3][eql], the column type *is* the configuration. Declare a column with the encrypted type +that names its data type and the operations it supports, and it's ready to query — there's no per-column +search configuration to maintain in your client: + +```sql +CREATE TABLE users ( + id serial PRIMARY KEY, + username text, -- plaintext — business as usual + email eql_v3_text_match, -- encrypted · free-text search + ssn eql_v3_text_eq, -- encrypted · equality + salary eql_v3_integer_ord, -- encrypted · range + ORDER BY + preferences eql_v3_json_search -- encrypted · containment + selectors +); +``` + +Encrypted types exist for text, integers, floats, numerics, dates, timestamps, booleans, and JSON, so +your schema documents itself — and encrypted data stays indexable with standard Postgres indexes. No +special index engine, no SQL rewrites. ORMs pick the types up transparently: declare the column as +encrypted in `schema.prisma` or your Drizzle table and the Stack handles the rest. Only raw `pg` needs +a client-side [schema][schema] — declared with the same type names: ```typescript -import { Encryption, encryptedTable, types } from "@cipherstash/stack/v3"; +import { encryptedTable, types } from "@cipherstash/stack/v3"; -// 1. Define your schema — the column type fixes its query capabilities const users = encryptedTable("users", { - email: types.TextSearch("email"), // equality + order/range + free-text search + email: types.TextMatch("email"), // ↔ eql_v3_text_match + salary: types.IntegerOrd("salary"), // ↔ eql_v3_integer_ord }); +``` + +→ [Searchable encryption][searchable-encryption] · [Schema][schema] · [Encrypt & decrypt][encrypt-decrypt] · [Bulk & model operations][model-ops] + +### 🔑 Authentication + +How you authenticate to ZeroKMS depends on who's asking for keys: -// 2. Initialize the client +- **Device auth** — browser-based login for local development: `npx stash auth login` opens your + browser and saves credentials to your local CipherStash profile. No secrets in your repo or shell. +- **Access key auth** — service-level credentials for servers, workers, and CI, supplied via `CS_*` + environment variables. +- **OIDC federation** — federate your identity provider's JWT so every key request authenticates *as the + signed-in user*, not as your app. Supported providers: **Supabase Auth**, **Clerk**, **Okta**, and **Auth0** + (any OIDC-compliant provider works). + +```typescript +// Access key (default) — reads CS_* env vars, no config needed const client = await Encryption({ schemas: [users] }); -// 3. Encrypt -const encryptResult = await client.encrypt("secret@example.com", { - column: users.email, - table: users, +// OIDC federation — every ZeroKMS request authenticates as the end user +const client = await Encryption({ + schemas: [users], + config: { authStrategy: OidcFederationStrategy.create(workspaceCrn, () => getUserJwt()) }, }); -if (encryptResult.failure) { - // Handle errors your way -} +``` + +→ [Authentication][auth] + +### 🗝️ Built-in key management + +Key management is built in, powered by [ZeroKMS][zerokms] — no AWS KMS to wire up, no key vault to run, +no rotation schedule to babysit: + +- **A unique key for every value** — not one key per table or per database. +- **Automatic key rotation** — handled for you, with zero downtime. +- **CipherStash can never see your keys.** Keys are *derived inside your application*; neither plaintext + keys nor plaintext data ever leave your infrastructure. +- **Fast at scale** — bulk key operations handle up to 10,000 keys in a single call, up to 14× faster + than AWS KMS at peak ([benchmarks][benches]). +- **Every decryption is logged** — a built-in audit trail of who decrypted what, and when. + +> **CipherStash never sees your plaintext — or your keys.** Data is encrypted in your app with a unique +> key per value, and keys are derived inside your application via [ZeroKMS][zerokms] — so a database +> breach leaks only ciphertext. [See the security architecture →][security-architecture] + +## Advanced features + +### 👤 Identity-locking encryption -// 4. Decrypt -const decryptResult = await client.decrypt(encryptResult.data); -if (decryptResult.failure) { - // Handle errors your way +Building on OIDC federation, you can bind a record's encryption key to the end user's identity, so only +*that* user can decrypt their data: `.withLockContext({ identityClaim })` ties the data key to a claim in +the user's JWT, enforced cryptographically by ZeroKMS. + +```typescript +// Bind the data key to a claim — the same claim is required to decrypt +await client + .encrypt("alice@example.com", { table: users, column: users.email }) + .withLockContext({ identityClaim: ["sub"] }); +``` + +→ [Identity-locking encryption][identity] + +### 🗂️ Keysets for multitenancy & sovereignty + +Partition your keys into **keysets** — independent key hierarchies within a single workspace. Give each +tenant its own keyset (`config.keyset`) for cryptographic tenant isolation: every encrypt, decrypt, and +query is scoped to a keyset, so revoking a client's access to a keyset makes that tenant's data +unreadable until the grant is restored — without re-architecting your app. [Keysets →][keysets] + +## Encrypted fields. Real queries. Your tools. + +The `email` column below is stored as ciphertext with a unique key per row — and the search still works, +because the query runs on the ciphertext. No decrypt-and-scan, no query rewrites. + +**Supabase** — same Supabase.js calls; the wrapper introspects your schema, encrypts filters on the way +in, and decrypts results on the way out: + +```typescript +const db = await encryptedSupabase(supabaseUrl, supabaseKey); + +const { data } = await db.from("users") + .select("id, name, email") + .eq("email", "alice@acme.com"); // encrypted equality — runs on ciphertext +``` + +**Prisma Next** — declare encrypted columns in `schema.prisma`, query with type-safe operators: + +```prisma +model User { + id String @id + email cipherstash.TextSearch() } -// decryptResult.data => "secret@example.com" ``` -## Install +```typescript +const rows = await db.orm.public.User + .where((u) => u.email.eqlMatch("acme.com")) + .all(); +``` -```bash -npm install @cipherstash/stack -# or -yarn add @cipherstash/stack -# or -pnpm add @cipherstash/stack -# or -bun add @cipherstash/stack +**Drizzle** — encrypted column types in your table, auto-encrypting operators in your queries: + +```typescript +export const usersTable = pgTable("users", { + id: integer("id").primaryKey(), + email: types.TextSearch("email"), // → eql_v3_text_search — the type is the config +}); + +const results = await db.select().from(usersTable) + .where(await ops.matches(usersTable.email, "acme.com")); ``` -> [!IMPORTANT] -> **You need to opt out of bundling when using `@cipherstash/stack`.** -> It uses Node.js specific features and requires the native Node.js `require`. -> Read more about bundling in the [documentation](https://cipherstash.com/docs/stack/deploy/bundling). +## How it works + +

+ + + + + + + + + CipherStash architecture: encryption and decryption happen in your TypeScript app; only ciphertext (EQL JSON) is stored in your PostgreSQL database. ZeroKMS issues a unique key per value, derived in your app. Plaintext and keys never reach CipherStash, and every decryption is logged for audit. + +

+ +Encryption happens in your application. Ciphertext is stored as an [EQL][eql] payload in your database; +plaintext and keys never reach CipherStash. Per-value keys are issued in bulk by ZeroKMS (so millions +of unique keys stay fast), and every decryption is logged for compliance. + +→ [Security architecture][security-architecture] · [ZeroKMS][zerokms] + +## Performance + +Encrypted queries stay fast — latency is flat from 10k to 10M rows. Measured on EQL v3 in [cipherstash/benches][benches]: + +| Operation | Median latency (up to 10M rows) | +|---|---| +| Equality lookup | ~0.1 ms | +| Range query | ~0.5 ms | +| JSON field equality | ~0.1 ms | + + + +## Why CipherStash + +- **Trusted data access** — only your end-users can access their sensitive data, enforced cryptographically. +- **Shrink the blast radius** — a breached vulnerability exposes only what one user can decrypt, not your whole table. +- **Audit trail built in** — every decryption event is recorded, no extra tooling to bolt on. +- **Meet compliance faster** — exceed the encryption requirements of SOC 2 and ISO 27001, with FIPS-compliant + cryptography and BYOK for teams that need it. -## Features +## FAQ -- **[Searchable encryption](https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption)**: query encrypted data with equality, free text search, range, and [JSONB queries](https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption#jsonb-queries-with-searchablejson). -- **[Type-safe schema](https://cipherstash.com/docs/stack/cipherstash/encryption/schema)**: define encrypted tables and columns with `encryptedTable` and the `types.*` concrete-domain factories -- **[Model & bulk operations](https://cipherstash.com/docs/stack/cipherstash/encryption/encrypt-decrypt#model-operations)**: encrypt and decrypt entire objects or batches with `encryptModel` / `bulkEncryptModels`. -- **[Identity-aware encryption](https://cipherstash.com/docs/stack/cipherstash/encryption/identity)**: authenticate as the end user with `OidcFederationStrategy` and bind the data key to their identity with `.withLockContext({ identityClaim })` for policy-based access control. +
+Can CipherStash ever see my data, or my encryption keys? -## Integrations +No, never. Encryption and decryption happen in your application, and keys are derived within your own +environment. Plaintext and keys never leave your control and never reach CipherStash. +
-- [Encryption + Drizzle](https://cipherstash.com/docs/stack/cipherstash/encryption/drizzle) -- [Encryption + Supabase](https://cipherstash.com/docs/stack/cipherstash/encryption/supabase) -- [Encryption + DynamoDB](https://cipherstash.com/docs/stack/cipherstash/encryption/dynamodb) +
+How well does it scale? -## Use cases +Latency stays flat as data grows — exact-match lookups hold at ~0.1 ms and range queries at ~0.5 ms from +10k up to 10M rows ([cipherstash/benches][benches]). ZeroKMS handles keys in bulk (up to 10,000 per +call), so key management isn't the bottleneck. +
-- **Trusted data access**: ensure only your end-users can access their sensitive data using identity-bound encryption -- **Reduce breach impact**: limit the blast radius of exploited vulnerabilities to only the data the affected user can decrypt +
+What does migration look like? -## Documentation +Install EQL on your Postgres database (`npx stash init` and the [quick starts](#quick-starts) handle +this), declare the columns you want protected with the encrypted type that fits each one (for example +`eql_v3_text_match` for searchable text or `eql_v3_integer_ord` for range queries), and encrypt values in +your app before writing. You can adopt it column-by-column — no big-bang rewrite — and your existing +Postgres indexes keep working. +
-- [Documentation](https://cipherstash.com/docs) -- [Quickstart](https://cipherstash.com/docs/stack/quickstart) -- [SDK and API reference](https://cipherstash.com/docs/stack/reference) +
+Do I have to change how I write queries? + +Barely. You keep your query builder: Supabase.js filters work unchanged, and Drizzle and Prisma Next +add encrypted-aware operators (`ops.eq`, `ops.matches`, `eqlMatch`, …) that take plaintext and encrypt +it for you. There are no SQL rewrites. +
+ +
+Do I need to run a KMS or key vault? + +No. Key management is built in through ZeroKMS. If you want to control the root key, Bring Your Own Key +lets you root it in your own KMS. +
+ +
+Does it work with Supabase Auth and Row Level Security? + +Yes. It integrates with Supabase Auth and runs alongside RLS — it complements them, it doesn't replace +them. +
+ +
+I already use Row Level Security — do I need this? + +RLS and CipherStash solve different problems, and they're strongest together. RLS decides which rows a +role may query, but the data underneath is plaintext — so anything that bypasses RLS reveals it in the +clear: a leaked `service_role` key, a misconfigured policy, a SQL injection running as an elevated role, +a stolen backup, or the database host itself. CipherStash stores only ciphertext and keeps the keys +outside the database, so those same bypasses reveal nothing readable. Keep RLS for authorization; add +CipherStash so a bypass never becomes a breach. +
+ +
+Is there a free tier? + +Yes — a free developer tier, so you can build encryption in from day one. +
+ +## Start free + +Encryption is far cheaper to design in than to retrofit — and it's what unlocks regulated and enterprise +customers. The developer tier is **free**, so you can add encryption from your very first migration: + +```bash +npx stash init +``` + +Signing up is the wizard's first step if you don't have an account yet — or +[create your free account][signup] in the browser first, and `stash init` will pick it up. + +## Install + +```bash +npm install @cipherstash/stack # or: yarn / pnpm / bun add @cipherstash/stack +``` + +> [!IMPORTANT] +> **Opt out of bundling `@cipherstash/stack`.** It uses native Node.js features (a Rust FFI module) and the +> native `require`. For edge and serverless runtimes (Cloudflare Workers, Deno, Bun), use the bundler-friendly +> `@cipherstash/stack/wasm-inline` entry instead. [Bundling guide →][bundling] -## Contributing +**Requirements:** Node.js ≥ 22. -Contributions are welcome and highly appreciated. However, before you jump right into it, we would like you to review our [Contribution Guidelines](CONTRIBUTE.md) to make sure you have a smooth experience contributing. +## Documentation & community -## Security +- 📚 [Documentation][docs] · [Quickstart][quickstart] · [SDK reference][reference] +- 🧩 [Example apps][examples] +- 💬 [Discord community][discord] -For our full security policy, supported versions, and contributor guidelines, see [SECURITY.md](./SECURITY.md). +## Contributing · Security · License -## License +Contributions are welcome — see [CONTRIBUTE.md][contribute]. For our security policy and responsible +disclosure, see [SECURITY.md][security-policy]. [MIT licensed][license]. -This project is [MIT licensed](./LICENSE.md). + +[signup]: https://cipherstash.com/signup?utm_source=github&utm_medium=stack_readme +[docs]: https://cipherstash.com/docs/stack?utm_source=github&utm_medium=stack_readme +[quickstart]: https://cipherstash.com/docs/stack/quickstart?utm_source=github&utm_medium=stack_readme +[reference]: https://cipherstash.com/docs/stack/reference?utm_source=github&utm_medium=stack_readme +[encryption]: https://cipherstash.com/docs/stack/cipherstash/encryption?utm_source=github&utm_medium=stack_readme +[searchable-encryption]: https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption?utm_source=github&utm_medium=stack_readme +[schema]: https://cipherstash.com/docs/stack/cipherstash/encryption/schema?utm_source=github&utm_medium=stack_readme +[encrypt-decrypt]: https://cipherstash.com/docs/stack/cipherstash/encryption/encrypt-decrypt?utm_source=github&utm_medium=stack_readme +[model-ops]: https://cipherstash.com/docs/stack/cipherstash/encryption/encrypt-decrypt?utm_source=github&utm_medium=stack_readme#model-operations +[supabase]: https://cipherstash.com/docs/stack/cipherstash/encryption/supabase?utm_source=github&utm_medium=stack_readme +[drizzle]: https://cipherstash.com/docs/stack/cipherstash/encryption/drizzle?utm_source=github&utm_medium=stack_readme +[prisma-next]: https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next?utm_source=github&utm_medium=stack_readme +[dynamodb]: https://cipherstash.com/docs/stack/cipherstash/encryption/dynamodb?utm_source=github&utm_medium=stack_readme +[identity]: https://cipherstash.com/docs/stack/cipherstash/encryption/identity?utm_source=github&utm_medium=stack_readme +[security-architecture]: https://cipherstash.com/docs/stack/reference/security-architecture?utm_source=github&utm_medium=stack_readme +[zerokms]: https://cipherstash.com/docs/stack/cipherstash/kms?utm_source=github&utm_medium=stack_readme +[bundling]: https://cipherstash.com/docs/stack/deploy/bundling?utm_source=github&utm_medium=stack_readme +[query-equality]: https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption?utm_source=github&utm_medium=stack_readme#equality +[query-match]: https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption?utm_source=github&utm_medium=stack_readme#free-text-search +[query-range]: https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption?utm_source=github&utm_medium=stack_readme#range-and-ordering +[query-json]: https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption?utm_source=github&utm_medium=stack_readme#json +[auth]: https://cipherstash.com/docs/stack/cipherstash/encryption/identity?utm_source=github&utm_medium=stack_readme +[keysets]: https://cipherstash.com/docs/stack/cipherstash/kms?utm_source=github&utm_medium=stack_readme#keysets +[benches]: https://github.com/cipherstash/benches +[eql]: https://github.com/cipherstash/encrypt-query-language +[discord]: https://discord.gg/5qwXUFb6PB +[examples]: https://github.com/cipherstash/stack/tree/main/examples +[contribute]: https://github.com/cipherstash/stack/blob/main/CONTRIBUTE.md +[security-policy]: https://github.com/cipherstash/stack/blob/main/SECURITY.md +[license]: https://github.com/cipherstash/stack/blob/main/LICENSE.md diff --git a/docs/images/architecture-dark.svg b/docs/images/architecture-dark.svg new file mode 100644 index 000000000..9539c1630 --- /dev/null +++ b/docs/images/architecture-dark.svg @@ -0,0 +1,4 @@ + + + + YOUR TRUST BOUNDARY — PLAINTEXT AND DATA KEYS NEVER LEAVEYOUR APP[TYPESCRIPT]@cipherstash/stack· encrypt / decrypt in-app· search on ciphertextDATA KEY DERIVATION[LOCAL]your client key — unique to this app+ key component from ZeroKMS= per-value data keysderived here — never leave the appYOUR DATABASE[POSTGRESQL]PostgreSQL · JSONB· stores ciphertext only· searchable via EQLZEROKMS[CIPHERSTASH]holds one key component — never a whole key· unique key component per value· bulk key ops — fastEVERY DECRYPTION LOGGED → SOC 2 / ISO 27001CIPHERSTASH NEVER SEES PLAINTEXT OR DATA KEYSciphertext — EQL JSON payloadencrypted rowsper-value key requests (bulk)key component — ½ of each key \ No newline at end of file diff --git a/docs/images/architecture-light.svg b/docs/images/architecture-light.svg new file mode 100644 index 000000000..f5fe3404a --- /dev/null +++ b/docs/images/architecture-light.svg @@ -0,0 +1,4 @@ + + + + YOUR TRUST BOUNDARY — PLAINTEXT AND DATA KEYS NEVER LEAVEYOUR APP[TYPESCRIPT]@cipherstash/stack· encrypt / decrypt in-app· search on ciphertextDATA KEY DERIVATION[LOCAL]your client key — unique to this app+ key component from ZeroKMS= per-value data keysderived here — never leave the appYOUR DATABASE[POSTGRESQL]PostgreSQL · JSONB· stores ciphertext only· searchable via EQLZEROKMS[CIPHERSTASH]holds one key component — never a whole key· unique key component per value· bulk key ops — fastEVERY DECRYPTION LOGGED → SOC 2 / ISO 27001CIPHERSTASH NEVER SEES PLAINTEXT OR DATA KEYSciphertext — EQL JSON payloadencrypted rowsper-value key requests (bulk)key component — ½ of each key \ No newline at end of file diff --git a/docs/images/architecture-stacked-dark.svg b/docs/images/architecture-stacked-dark.svg new file mode 100644 index 000000000..0eeddd76d --- /dev/null +++ b/docs/images/architecture-stacked-dark.svg @@ -0,0 +1,4 @@ + + + + YOUR TRUST BOUNDARY — PLAINTEXT + DATA KEYS NEVER LEAVEYOUR APP[TYPESCRIPT]@cipherstash/stack· encrypt / decrypt in-app· search on ciphertextDATA KEY DERIVATION[LOCAL]your client key — unique to this app+ key component from ZeroKMS= per-value data keysderived here — never leave the appYOUR DATABASE[POSTGRESQL]PostgreSQL · JSONB· stores ciphertext only· searchable via EQLZEROKMS[CIPHERSTASH]holds one key component — never a whole key· unique key component per value· bulk key ops — fastEVERY DECRYPTION LOGGED → SOC 2 / ISO 27001NEVER SEES PLAINTEXT OR DATA KEYSciphertext · EQL JSONencrypted rowsper-value key requests (bulk)key component — ½ of each key \ No newline at end of file diff --git a/docs/images/architecture-stacked-light.svg b/docs/images/architecture-stacked-light.svg new file mode 100644 index 000000000..1aeab2534 --- /dev/null +++ b/docs/images/architecture-stacked-light.svg @@ -0,0 +1,4 @@ + + + + YOUR TRUST BOUNDARY — PLAINTEXT + DATA KEYS NEVER LEAVEYOUR APP[TYPESCRIPT]@cipherstash/stack· encrypt / decrypt in-app· search on ciphertextDATA KEY DERIVATION[LOCAL]your client key — unique to this app+ key component from ZeroKMS= per-value data keysderived here — never leave the appYOUR DATABASE[POSTGRESQL]PostgreSQL · JSONB· stores ciphertext only· searchable via EQLZEROKMS[CIPHERSTASH]holds one key component — never a whole key· unique key component per value· bulk key ops — fastEVERY DECRYPTION LOGGED → SOC 2 / ISO 27001NEVER SEES PLAINTEXT OR DATA KEYSciphertext · EQL JSONencrypted rowsper-value key requests (bulk)key component — ½ of each key \ No newline at end of file diff --git a/docs/plans/readme-go-live-checklist.md b/docs/plans/readme-go-live-checklist.md new file mode 100644 index 000000000..45915579d --- /dev/null +++ b/docs/plans/readme-go-live-checklist.md @@ -0,0 +1,72 @@ +# Root README — go-live checklist + +The refreshed root `README.md` started **doc-driven** — written slightly ahead of the code. EQL v3 has +since shipped, and the code examples were re-verified against `main` when the branch was rebased +(2026-07-30). What remains below is the pre-merge tail: link verification, benchmark refresh, and the +social preview card. + +## API surfaces shown in code examples — ✅ verified against `main` + +- [x] **Drizzle** — `types.TextSearch("email")` in `pgTable` and `ops.matches(...)` match + `@cipherstash/stack-drizzle` (there are no `like`/`ilike` operators on the v3 surface, by design). +- [x] **Supabase** — `await encryptedSupabase(supabaseUrl, supabaseKey)` (introspects the database at + connect time) with `.from("users").select(...).eq(...)`. The sizzle uses an equality filter because + `matches()` is unavailable through PostgREST on EQL 3.0.2. +- [x] **Prisma Next** — `cipherstash.TextSearch()` in `schema.prisma` and the + `db.orm.public.User.where((u) => u.email.eqlMatch(...))` operator surface + (`@cipherstash/stack-prisma`). +- [x] **Raw SDK** — `import { encryptedTable, types } from "@cipherstash/stack/v3"` with + `types.TextMatch` / `types.IntegerOrd`. +- [x] **Auth** — `config.authStrategy` (`config.strategy` is deprecated) with + `OidcFederationStrategy.create(crn, () => getJwt())`, matching + `packages/stack/__tests__/lock-context.test.ts`. + +## EQL v3 claims — ✅ verified against `main` + +- [x] **Domain type names** — column domains live in `public`: `eql_v3_text_match`, `eql_v3_text_eq`, + `eql_v3_integer_ord`, `eql_v3_json_search` (the `eql_v3` schema holds the `query_*` domains and + functions). CREATE TABLE example + FAQ updated to match. +- [x] **"The type is the configuration"** — Drizzle derives the client schema from the table + (`extractEncryptionSchema`), Prisma from `schema.prisma`, Supabase from database introspection; raw + `pg` is the only surface needing a hand-written client schema. +- [x] **Scalar coverage** — text, smallint/integer/bigint, real/double, numeric, date, timestamp, + boolean, and JSON all have `types.*` factories. +- [x] **Standard Postgres indexes** — holds; note the `*OrdOre` domains need a custom operator class + the installer can't create on cloud-hosted Supabase (plain `Ord` works everywhere). + +## CLI & auth claims + +- [x] **Device auth** — `stash auth login` is in the command registry (OAuth 2.0 device flow, local + profile). +- [ ] **`npx stash init`** — confirm sign-up-first flow for users without an account (Start free CTA). +- [ ] **OIDC federation providers** — Supabase Auth, Clerk, Okta, and Auth0 all verified working. + +## Key management claims + +- [ ] **Automatic key rotation** — shipped and accurate to describe as "handled for you, zero downtime". +- [x] **Keysets** — per-tenant keysets via `config.keyset` and loud failure on revoked access are + shipped (see `skills/stash-zerokms`). The README's earlier "pin keysets to a region" and + "permanently unreadable" claims were softened: region residency is a workspace property (part of the + CRN), not per-keyset, and grant revocation is reversible. +- [ ] **Audit logging** — "every decryption is logged" is true for all auth modes. + +## Performance section + +- [x] **Numbers refreshed on EQL v3** — verified against the EQL v3 run in `cipherstash/benches` + (`report/BENCHMARK_REPORT.md` + per-family pages): equality ~0.12–0.14 ms, ORE range (10 rows) + ~0.5 ms, JSON field equality ~0.1 ms — flat from 10k to 10M rows, matching the README table. +- [ ] **Flat-latency chart** embedded (spec: Asset 3 in `readme-visual-assets.md`). + +## Links & assets (mechanical, pre-merge) + +- [x] **Doc links** — all docs-site URLs checked against the published docs (2026-07-30), including + the query-type anchors and the per-integration quick-start slugs. The `[auth]` link now points at + the identity page (`/docs/stack/cipherstash/encryption/identity`) — there is no standalone + authentication page. +- [x] **Architecture diagram URLs** — the four `` paths use the absolute + `https://raw.githubusercontent.com/cipherstash/stack/main/…` prefix (they will 404 on the PR branch + and resolve on merge). +- [ ] **Social preview card** — upload the 1280×640 og:image via repo Settings → General → Social + preview, and set the About description to the brief's one-liner ("Searchable, application-level + encryption for building privacy-first apps.") — it doubles as the `og:description`. Verify with a + fresh link paste (spec: Asset 4 in `readme-visual-assets.md`). diff --git a/docs/plans/readme-visual-assets.md b/docs/plans/readme-visual-assets.md new file mode 100644 index 000000000..57b4511b1 --- /dev/null +++ b/docs/plans/readme-visual-assets.md @@ -0,0 +1,218 @@ +# README visual assets — spec + +Two visual assets for the refreshed root `README.md`. Both target the gaps competitor +READMEs leave open: + +1. **Architecture diagram** — security/infra READMEs (Infisical, Vault) bury their architecture + off-README. A clear "how it works" diagram is the single biggest trust signal we can add. +2. **Type-safety autocomplete GIF** — none of the TS-first leaders (Prisma, Drizzle, React Email) + *show* their type-safety story; they only describe it. An autocomplete GIF beats all of them. + +## Shared conventions + +- **Host in-repo** under `docs/images/` so assets are version-controlled. +- **Reference with absolute URLs** (`https://raw.githubusercontent.com/cipherstash/stack/main/docs/images/...`). + Relative paths render on GitHub but **break on the npm package page** — npm needs absolute URLs. +- **Light + dark variants** using GitHub's mode switch: + ```html + ... + ... + ``` +- **Brand**: use the CipherStash palette and logo; match the dark-theme look of cipherstash.com. +- **Accessibility**: every asset needs descriptive `alt` text (provided below). For the GIF, keep motion + calm and the loop short (respect users who dislike motion). + +--- + +## Asset 1 — Architecture diagram ("How it works") + +**Goal.** In one glance, prove the core trust claim: *plaintext and root keys never reach CipherStash; the +database only ever holds ciphertext; every decryption is audited.* + +**Placement.** Under the `## How it works` heading, above the "Security architecture" doc link. + +**Format.** SVG preferred (crisp, tiny, theme-able). Target ~1400px wide, responsive height. + +**Layout (left → right data flow):** + +```text +┌─────────────────────────┐ ciphertext ┌──────────────────────────┐ +│ YOUR APP (TypeScript) │ ── EQL JSON payload ──▶ │ YOUR DATABASE │ +│ @cipherstash/stack │ │ PostgreSQL / JSONB │ +│ • encrypt / decrypt │ ◀── encrypted rows ─── │ • stores ciphertext only│ +│ • search on ciphertext │ │ • searchable (EQL) │ +└───────────┬─────────────┘ └──────────────────────────┘ + │ per-value key requests (bulk) + ▼ +┌─────────────────────────┐ root key ┌──────────────────────────┐ +│ ZeroKMS │ ───────────▶ │ YOUR AWS KMS │ +│ • unique key per value │ │ • root key never leaves │ +│ • bulk key ops (fast) │ └──────────────────────────┘ +│ • decryption audit log │ +└─────────────────────────┘ +``` + +**Trust-boundary callouts to overlay (the persuasive part):** +- A dashed "trust boundary" line around *Your App + Your Database + Your AWS KMS* labelled + **"Plaintext and root keys never leave your boundary."** +- A badge on ZeroKMS: **"CipherStash never sees plaintext."** +- A small tag near the audit log: **"Every decryption logged → SOC 2 / ISO 27001 evidence."** + +**Alt text:** +> "CipherStash architecture: encryption and decryption happen in your TypeScript app; only ciphertext +> (EQL JSON) is stored in your PostgreSQL database. ZeroKMS issues a unique key per value, rooted in your +> own AWS KMS. Plaintext and root keys never reach CipherStash, and every decryption is logged for audit." + +**Tooling.** Figma, Excalidraw, or draw.io → export SVG (light + dark). Keep text as real text (not +outlines) where possible for crispness and accessibility. + +**Interim option (ship today, no designer needed).** GitHub renders Mermaid natively, so this can go in +immediately and be swapped for the designed SVG later: + +```mermaid +flowchart LR + App["Your App (TypeScript)
@cipherstash/stack
encrypt · decrypt · search"] + DB[("Your Database
PostgreSQL / JSONB
ciphertext only")] + ZKMS["ZeroKMS
unique key per value
bulk ops · audit log"] + KMS["Your AWS KMS
root key never leaves"] + + App -- "ciphertext (EQL JSON)" --> DB + App -- "per-value key requests" --> ZKMS + ZKMS -- "root key" --> KMS + + subgraph Boundary["Your trust boundary — plaintext & root keys never leave"] + App + DB + KMS + end +``` + +--- + +## Asset 2 — Type-safety / autocomplete GIF + +**Goal.** Show the DX payoff in motion: an encrypted field stays **fully typed and queryable** — encryption +adds security without taking away autocomplete, inference, or compile-time safety. + +**Placement.** Inside the `### 🔐 Searchable encryption` pillar, or a short "Developer experience" callout. + +**Storyboard (single seamless loop, ≤ 10s):** +1. Show a schema: `encryptedTable("users", { email: encryptedColumn("email").equality().freeTextSearch() })`. +2. Type `await client.encryptModel(user, users)` and hover the result — tooltip shows the **schema-aware + return type**: `email → Encrypted`, `id → string`, `createdAt → Date` (only schema fields change type). +3. Start typing a query: `.where(await ops.eq(usersTable.email, "` — show autocomplete offering the typed + operator and the column. +4. Briefly trigger a **red squiggle** by accessing a field not in the schema (or wrong type) — proving + errors are caught at compile time. + +**Recording specs:** +- VS Code, clean theme (record a **dark** primary; a light alt is nice-to-have). +- Font size 16–18px; minimap off; hide activity/status bar clutter; zoom so code is legible on mobile. +- Crop tight to the editor region. Width 1280–1440px. +- Length 8–12s, seamless loop. **File budget < 5 MB** (ideally < 3 MB) so the README stays fast. + +**Formats:** +- Ship a **`.gif`** for universal rendering (works on npm and GitHub). +- Optionally also provide an `.mp4`/`.webm` and embed via `