Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,306 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CipherStash

CipherStash Stack for TypeScript

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!

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), includes agent handoff.

# Run this to start (or just ask Claude to)
npx stash init

ORM/database-specific guides

Quick start Guide
Supabase Supabase quickstart →
Prisma Next Prisma Next quickstart →
Drizzle ORM Drizzle quickstart →
Raw PostgreSQL (pg) PostgreSQL quickstart →
DynamoDB DynamoDB quickstart →

The Stack also ships a stash CLI for auth, schema, and database setup. See the SDK reference.

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 →
Free-text search fuzzy matches Text search →
Range & ordering <, >, BETWEEN, ORDER BY, MIN/MAX Range queries →
Encrypted JSON containment (@>), JSONPath selectors JSON queries →

With EQL v3, 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:

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 — declared with the same type names:

import { encryptedTable, types } from "@cipherstash/stack/v3";

const users = encryptedTable("users", {
  email: types.TextMatch("email"),       // ↔ eql_v3_text_match
  salary: types.IntegerOrd("salary"),    // ↔ eql_v3_integer_ord
});

Searchable encryption · Schema · Encrypt & decrypt · Bulk & model operations

🔑 Authentication

How you authenticate to ZeroKMS depends on who's asking for keys:

  • 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).
// Access key (default) — reads CS_* env vars, no config needed
const client = await Encryption({ schemas: [users] });

// OIDC federation — every ZeroKMS request authenticates as the end user
const client = await Encryption({
  schemas: [users],
  config: { authStrategy: OidcFederationStrategy.create(workspaceCrn, () => getUserJwt()) },
});

Authentication

🗝️ Built-in key management

Key management is built in, powered by 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).
  • 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 — so a database breach leaks only ciphertext. See the security architecture →

Advanced features

👤 Identity-locking encryption

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.

// 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

🗂️ 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 →

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:

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:

model User {
  id    String @id
  email cipherstash.TextSearch()
}
const rows = await db.orm.public.User
  .where((u) => u.email.eqlMatch("acme.com"))
  .all();

Drizzle — encrypted column types in your table, auto-encrypting operators in your queries:

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"));

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 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 · ZeroKMS

Performance

Encrypted queries stay fast — latency is flat from 10k to 10M rows. Measured on EQL v3 in cipherstash/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.

FAQ

Can CipherStash ever see my data, or my encryption keys?

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.

How well does it scale?

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). ZeroKMS handles keys in bulk (up to 10,000 per call), so key management isn't the bottleneck.

What does migration look like?

Install EQL on your Postgres database (npx stash init and the 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.

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:

npx stash init

Signing up is the wizard's first step if you don't have an account yet — or create your free account in the browser first, and stash init will pick it up.

Install

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 →

Requirements: Node.js ≥ 22.

Documentation & community

Contributing · Security · License

Contributions are welcome — see CONTRIBUTE.md. For our security policy and responsible disclosure, see SECURITY.md. MIT licensed.