CipherStashDocs

Supabase

Field-level encryption for your Supabase project. Works with any Postgres client, the Supabase JS SDK, or Drizzle ORM.

Field-level encryption that works with your Supabase project out of the box.

Connect from the dashboard

You can connect Supabase from the CipherStash Dashboard instead of starting entirely from the CLI.

Open workspace integrations

In your workspace, go to Settings → Integrations and click Connect Supabase.

Authorize Supabase OAuth

Approve access for the CipherStash OAuth app. Tokens are stored encrypted on the workspace.

Use the setup hub

Select a Supabase project, verify EQL and OIDC readiness, configure Supabase as an OIDC provider with one click, and copy Stack onboarding commands plus a .env.local snippet tailored to that project.

You can also install CipherStash from the Supabase Marketplace — the dashboard supports both simple and signed redirect install flows.

Full reference: Supabase dashboard integration

How this works

What is EQL?

EQL (Encrypted Query Language) is a Postgres extension CipherStash installs into your database. It provides the eql_v2_encrypted column type and the internal functions that make encrypted search possible. You don't write EQL directly. The SDK handles it.

Why your columns must be eql_v2_encrypted

Encrypted values aren't strings or plain JSONB. They're structured ciphertext objects that hold the ciphertext plus optional search indexes. Postgres needs the column type so the SDK and Postgres agree on which functions handle inserts and queries.

What the CLI installs on Supabase

db install --supabase uses a Supabase-compatible EQL variant. It omits CREATE OPERATOR FAMILY (which requires superuser), and grants USAGE, table, routine, and sequence permissions on the eql_v2 schema to anon, authenticated, and service_role.

Packages

CipherStash splits its functionality across two packages: a runtime SDK that your application imports, and a CLI for setup and schema management.

PackageRoleInstall as
@cipherstash/stackRuntime encryption and decryption. Your application imports this to encrypt values before they're written to Supabase and decrypt them on the way back.dependency
stashSetup and schema management. Installs EQL into your database, scaffolds your encryption client, and generates migrations when your encryption schema changes. Comparable to Prisma CLI or Drizzle Kit.devDependency

Setup

Install the packages

Install the runtime SDK as a dependency and the CLI as a dev dependency.

npm install @cipherstash/stack
npm install -D stash

Run init --supabase

Init runs nearly silently, with prompts only when it can't make a sensible default choice:

  • Authenticates you with CipherStash via your browser (only when you aren't already logged in).
  • Resolves your database connection via DATABASE_URL.
  • Generates an encryption client at ./src/encryption/index.ts. Only prompts you if a file already exists at that path.
  • Installs @cipherstash/stack and stash if either is missing — one combined prompt, skipped entirely when both are already present.
  • Installs EQL into your database.

The --supabase flag tailors the next-steps output to Supabase users.

npx stash init --supabase

Control how EQL is installed (optional)

stash init installs EQL automatically. If you need to control the install method — for example, to write a Supabase migration file instead of pushing directly — run stash db install with explicit flags.

The CLI prompts you to choose how EQL is installed. If a supabase/migrations/ directory is detected, the migration-file option is pre-selected.

Pass --migration to write the EQL SQL into a Supabase migration file, or choose "Create a Supabase migration file" at the prompt.

npx stash db install --supabase --migration

The CLI writes the EQL SQL to:

supabase/migrations/00000000000000_cipherstash_eql.sql

The all-zero timestamp prefix ensures this migration runs before any user migrations that reference eql_v2_encrypted. Run supabase db reset or supabase migration up to apply it.

To write the migration file to a different directory, use --migrations-dir:

npx stash db install --supabase --migration --migrations-dir ./db/migrations

--migration, --direct, and --migrations-dir all require --supabase to be passed explicitly. Passing them without --supabase will error.

Direct push

Pass --direct to push EQL directly to the database without creating a migration file.

npx stash db install --supabase --direct

Direct-push installs do not survive supabase db reset. The reset command drops the database and reruns only files in supabase/migrations/. EQL installed directly is not in migrations and will be wiped. Use the migration-file path for projects that use supabase db reset. See Supabase db reset.

If you hit issues with supabase db reset wiping EQL, see Supabase db reset removes EQL.

Database schema

Encrypted columns must use the eql_v2_encrypted type. Define them in your Supabase SQL editor or a migration file.

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email eql_v2_encrypted NOT NULL,
  name eql_v2_encrypted NOT NULL
);

Choose your integration

Three paths. Same encryption, same key management, same searchable queries. Pick the one that fits your stack.

Encryption SDK (any Postgres client)

Works with any client that connects to your Supabase Postgres database. Define a schema, encrypt fields, store and query with raw SQL or any library.

import { Encryption, encryptedTable, encryptedColumn } from "@cipherstash/stack"

const users = encryptedTable("users", {
  email: encryptedColumn("email").equality().freeTextSearch(),
})

const enc = await Encryption({ schemas: [users] })

// Encrypt
const encrypted = await enc.encrypt("[email protected]", {
  column: users.email, table: users,
})

// Store with any client
await sql`INSERT INTO users (email) VALUES (${encrypted.data})`

// Query — search ciphertext without decrypting
const query = await enc.encryptQuery("[email protected]", {
  column: users.email, table: users,
})

This is the universal approach. It works regardless of which client library you use to connect to Supabase.

Full reference: Storing encrypted data

Supabase JS SDK

If you're using the Supabase JavaScript client, the encryptedSupabase wrapper gives you automatic encryption with the same API you already know.

import { encryptedSupabase } from "@cipherstash/stack/supabase"

const eSupabase = await encryptedSupabase(supabaseUrl, supabaseKey)

// Insert — automatically encrypted
await eSupabase.from("users").insert({
  email: "[email protected]",
})

// Query — automatically encrypts the search term
const { data } = await eSupabase
  .from("users")
  .select()
  .eq("email", "[email protected]")

The wrapper handles encryption, decryption, type casting, and search term formatting. Your queries look identical to standard Supabase queries.

Full reference: Supabase JS SDK integration

Drizzle ORM

If you're using Drizzle ORM with Supabase, define encrypted columns directly in your Drizzle schema.

import { encryptedType } from "@cipherstash/stack/drizzle"

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: encryptedType<string>("email"),
})

Encrypted query operators work like standard Drizzle operators.

Full reference: Drizzle ORM integration

What you get

  1. Searchable encryption. Equality, free-text search, range, ordering, and JSON queries over ciphertext. Your Postgres indexes work on encrypted data.
  2. Works alongside Row Level Security. CipherStash encryption and Supabase RLS are complementary. RLS controls who can access rows. CipherStash controls who can decrypt values. Defense in depth.
  3. Schema-first. Define encrypted columns once. Type-safe across your entire application.
  4. Identity-bound keys. Tie encryption to a user's identity. Only that user can decrypt their data.
  5. Zero-knowledge. CipherStash never sees your plaintext data. Keys are derived on your device and never stored.

Encrypted columns on Supabase require functional indexes and a specific query form to avoid silent sequential scans. See Setting up indexes for the correct CREATE INDEX statements and query patterns.

Going to production

Local development uses device-based authentication. Production uses environment variables. See Going to production.

Next steps

On this page