@cipherstash/stack-supabase
v1.1.1
Published
CipherStash Stack Supabase integration: transparent, searchable field-level encryption for Supabase.
Readme
Why
Anyone with database access — a leaked service_role key, a misconfigured RLS policy, a SQL
injection, a stolen backup — normally sees everything. CipherStash encrypts each value with its
own key before it leaves your app, so what reaches Supabase is ciphertext; you can only decrypt
what you're explicitly authorized to, and every decryption is audited.
The trick is queries still work: searchable encrypted terms let equality and range filters run against native Postgres indexes without decrypting the table. RLS stays exactly where it is — this complements authorization, it doesn't replace it. Security architecture →
Encrypted columns. Real Supabase queries.
The email and amount columns below are stored as ciphertext with a unique key per row — and
the same Supabase.js calls keep working, because the filters run on the ciphertext:
import { encryptedSupabase } from '@cipherstash/stack-supabase'
const es = await encryptedSupabase(supabaseUrl, supabaseKey)
// Insert — encrypted transparently on the way in
await es.from('users').insert({ email: '[email protected]', amount: 30 })
// Query — filters encrypted on the way in, results decrypted on the way out
const { data } = await es
.from('users')
.select('id, email, amount')
.eq('email', '[email protected]') // encrypted equality — runs on ciphertext
.gte('amount', 10) // encrypted rangeYou can also wrap an existing client: await encryptedSupabase(supabaseClient, options).
| Query type | Filters | Notes |
|---|---|---|
| Equality | .eq, .neq, .in, .match({ … }) | on equality-capable domains |
| Range | .gt / .gte / .lt / .lte | on *Ord domains |
| Ordering | .order() | on OPE-backed encrypted ordering columns (and plaintext columns) |
| Compound | .or(…) | over the filters above |
Each column's query capabilities are fixed by its eql_v3_* type, so an unsupported operation is
rejected loudly instead of silently scanning.
PostgREST limitation (EQL 3.0.4). Encrypted free-text
matches(), encrypted JSONcontains(), andselectorEq()/selectorNe()need typed query-domain casts that PostgREST cannot express, so they fail fast with this EQL release — the requirement began in EQL 3.0.2 and remains in 3.0.4. Use the Drizzle or Prisma adapter, or a carefully scoped SQL/RPC path, for those query shapes. Plaintextlike/ilikeon encrypted columns is rejected by design.
Quick start
About five minutes, starting on the free developer tier (sign up). The setup wizard handles authentication, the EQL install, and your schema:
npx stash initOr install manually (this package depends on @cipherstash/stack; install all three):
npm install @cipherstash/stack @cipherstash/stack-supabase @supabase/supabase-jsFull guide: Supabase quickstart →
How the wrapper works
encryptedSupabase introspects your database at connect time: it discovers the native
public.eql_v3_* column domains, so there is no schema argument and no client-side column
config to maintain — select('*') just works, inserts and updates encrypt automatically, and
reads decrypt automatically.
Introspection needs a direct Postgres connection (DATABASE_URL), so pg is an optional peer
dependency and the factory cannot run in an edge Worker or the browser — construct it in your
server-side code.
It runs alongside Supabase Auth and RLS, and supports identity-locking encryption — binding a row's data key to the signed-in user's JWT claim — via the same lock-context API as the rest of the Stack.
encryptedSupabaseV3remains as a@deprecated, type-identical alias ofencryptedSupabase, so existing imports keep working.
How it works
Every value is encrypted into an EQL payload: the ciphertext plus the searchable terms
its column type declares — an HMAC term for equality, an order-preserving term for range and
sorting. The EQL SQL bundle defines the Postgres domains and operators, so an encrypted .eq()
resolves to a comparison of equality terms and engages a functional index. Keys come from
ZeroKMS — one per value — so a leaked key or a dumped table never exposes more than
it should, and the EQL install needs no superuser (it works on cloud-hosted Supabase as-is).
Docs
- Supabase integration guide →
- Searchable encryption concepts →
- Security architecture →
- The bundled
stash-supabaseagent skill, installed into your repo bystash init
