CipherStashDocs
IntegrationsSupabase

Quickstart

Encrypt, store, and query your first field in Supabase with EQL and the encrypted Supabase JavaScript client.

This tutorial creates a Supabase table with one encrypted column, writes a value through the Supabase JavaScript client, and queries it without sending plaintext to Postgres.

It is designed for a new table or an empty column. If the column already contains production data, use Data migration instead.

Before you start

You need:

  • A Supabase project and its database connection string
  • A Node.js project
  • A CipherStash account; the setup flow signs you in

Initialize CipherStash

Add your Supabase settings to the project's environment:

.env
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_ANON_KEY=...
DATABASE_URL=postgresql://postgres:...@db.<project-ref>.supabase.co:5432/postgres

Then run the Supabase setup flow:

npx stash init --supabase

stash init:

  • Opens the CipherStash device login if you do not already have a developer profile
  • Resolves the database configuration
  • Installs compatible, pinned versions of @cipherstash/stack, @cipherstash/stack-supabase, and the stash CLI
  • Installs EQL v3 with the grants required by Supabase's anon, authenticated, and service_role roles
  • Scaffolds the encryption client and records context for coding agents

If the command generates a Supabase migration instead of applying EQL directly, run the apply command it prints before continuing. Confirm that EQL is available with:

npx stash eql status

The native Stack client automatically uses the developer profile created by this login. You do not need the four deployment environment variables for local development. When you deploy, choose a deployment credential source.

Using an agent? An agent can drive the same setup. If no developer profile exists, it should start npx stash auth login --json --region <region> --supabase, show you the returned verification URL, and leave the command running until authorization completes. It can then run npx stash init --supabase --region <region>.

At the end of interactive initialization, you can continue into stash plan. Choosing Codex or Claude Code installs the Supabase-specific CipherStash skills into the agent's project directory; editor agents receive the same guidance through AGENTS.md. The skills cover Supabase, authentication, EQL indexes, deployment, and the CLI, giving the agent the context needed to plan and implement encryption changes safely.

Create an encrypted column

Run this in the Supabase SQL Editor or add it to a new migration:

CREATE TABLE contacts (
    id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email        public.eql_v3_text_eq NOT NULL,
    display_name text
);

email is an EQL equality column, so it can be stored, decrypted, and queried with methods such as .eq() and .in(). display_name is ordinary plaintext and passes through unchanged.

The column type is the encryption schema. There is no separate application declaration to keep synchronized. For other data types and query capabilities, see EQL core concepts.

Create the encrypted Supabase client

stash init installed the CipherStash packages. If this project does not already use the Supabase JavaScript client, install it:

npm install @supabase/supabase-js

Create a client:

lib/db.ts
import { encryptedSupabase } from "@cipherstash/stack-supabase"

export const db = await encryptedSupabase(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
)

At startup, encryptedSupabase uses DATABASE_URL to inspect the database and recognize contacts.email as encrypted. Plaintext tables and columns continue to behave normally.

Insert and query a value

Write plaintext application values as usual:

import { db } from "./lib/db"

const { error: insertError } = await db.from("contacts").insert({
  email: "[email protected]",
  display_name: "Alice",
})

if (insertError) throw insertError

The wrapper encrypts email before it leaves the application. It then encrypts the filter value when you query the column and decrypts the matching result:

const { data, error } = await db
  .from("contacts")
  .select("id, email, display_name")
  .eq("email", "[email protected]")
  .single()

if (error) throw error

console.log(data)
// { id: 1, email: "[email protected]", display_name: "Alice" }

Supabase Row Level Security still applies because the wrapper sends the request through the Supabase client. Add policies just as you would for an unencrypted table.

Confirm that Supabase sees ciphertext

Open contacts in the Supabase Table Editor. The email cell contains an EQL ciphertext payload, while display_name remains readable.

The Table Editor cannot decrypt or meaningfully filter the encrypted value because its keys remain outside the database. Read and update encrypted fields through a CipherStash-enabled application.

Next steps

On this page