Searchable, application-level encryption for building privacy-first apps.
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
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| 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
stashCLI for auth, schema, and database setup. See the SDK reference.
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
How you authenticate to ZeroKMS depends on who's asking for keys:
- Device auth — browser-based login for local development:
npx stash auth loginopens 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()) },
});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 →
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"] });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 →
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 ciphertextPrisma 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"));
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
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 |
- 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.
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.
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 initSigning 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.
npm install @cipherstash/stack # or: yarn / pnpm / bun add @cipherstash/stackImportant
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.
Contributions are welcome — see CONTRIBUTE.md. For our security policy and responsible disclosure, see SECURITY.md. MIT licensed.