EQL
Encrypt Query Language (EQL) installs encrypted column types and operators into Postgres as plain SQL — encryption itself happens in your client.
EQL version
Encrypt Query Language (EQL) is a set of types, operators, and functions for storing and querying encrypted data in PostgreSQL. The Stash CLI installs it over an ordinary database connection — no extension packaging, no superuser, no operator classes — so it runs on Supabase, RDS, Cloud SQL, and self-hosted Postgres alike.
EQL itself never encrypts anything. Encryption and decryption happen in the client, using the Stack SDK or CipherStash Proxy. EQL provides the database-side surface those clients query against: encrypted column types, the operators that compare them, and the term-extractor functions that make indexes work.
Every encrypted column is a jsonb-backed domain type in the public schema (the functions and operators behind them live in eql_v3), and the domain variant you choose declares what the column can do — the full model is in Core concepts.
Install
Run the installer
Point the Stash CLI at your database and install the eql_v3 schema:
npx stash eql installstash eql install scaffolds a stash.config.ts if your project doesn't have one, then installs the eql_v3 schema — all domain types, operators, functions, and aggregates — along with the cs_migrations tracking schema that the stash encrypt commands depend on. It is idempotent: re-running reports that EQL is already installed and changes nothing. Pass --force to reinstall in place.
EQL v3 is the only installation target in stash 1.0.0, so no version flag is required. The EQL v2 reference remains available for applications maintaining an existing v2 deployment.
Point it at each database
The CLI reads the connection string from DATABASE_URL (your environment or a .env file) and prompts if it can't find one. Override it for a single run — handy when installing across several databases — with --database-url, which is used for that run only and never written to disk:
npx stash eql install \
--database-url postgres://user:pass@host:5432/mydbRun the installer against every database that stores encrypted columns.
To pull in a newer EQL release later, run npx stash eql upgrade.
To uninstall, drop both EQL schemas: DROP SCHEMA eql_v3, eql_v3_internal CASCADE. This removes the encrypted operators and functions, so queries against encrypted columns stop resolving — but your data survives: the public.* domain types (and the columns typed as them) live in the public schema and are left untouched. Remove those separately only if you intend to drop or re-type the columns.
Configuration
stash eql install reads a stash.config.ts at your project root, scaffolding one on first run if it's missing. The same file is used by the stash db and stash encrypt commands. It exports a defineConfig object with two fields:
| Field | Required | Description |
|---|---|---|
databaseUrl | Yes | PostgreSQL connection string. The scaffold sets it to await resolveDatabaseUrl(), which resolves from the --database-url flag, then DATABASE_URL (environment or .env), then supabase status, then an interactive prompt. The resolved connection string is never written to disk — only the declarative call is. |
client | No | Path to your encryption client file. Defaults to ./src/encryption/index.ts. |
import { defineConfig, resolveDatabaseUrl } from "stash";
export default defineConfig({
databaseUrl: await resolveDatabaseUrl(),
client: "./src/encryption/index.ts",
});Drizzle migration history
For a Drizzle project, generate a custom migration instead of installing EQL directly:
npx stash eql migration --drizzle
npx drizzle-kit migrateThe command runs your project-local drizzle-kit, creates a custom migration, and writes the same EQL v3 SQL that stash eql install applies directly. Add --supabase when the database is also reached through Supabase PostgREST, and use --out when your Drizzle migration directory is not drizzle/.
If drizzle-kit generate has already emitted an in-place ALTER COLUMN ... SET DATA TYPE for an encrypted domain, repair the unapplied migration before running it:
npx stash eql repair --drizzle --database-url "$DATABASE_URL"See the eql migration and eql repair references for all options. Prisma ORM 8 installs EQL through its own migration graph and does not use either command.
dbdev
EQL is also published to dbdev. The dbdev release can lag behind GitHub releases, so prefer stash eql install when you need the latest version.
Docker for local development
Run a Postgres image with EQL pre-installed:
docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres \
ghcr.io/cipherstash/postgres-eql:17EQL installs automatically on first boot. Images are available for PostgreSQL 14–17 (:14 through :17), and you can pin a specific EQL version with a suffixed tag (for example :17-3.0.4).
Permissions
Installing EQL and running queries against it need different privileges. A common production pattern splits them across two users.
Migration user — installs EQL and adds encrypted columns during migrations:
GRANT CREATE ON DATABASE your_database TO your_migration_user;
GRANT CREATE ON SCHEMA public TO your_migration_user;
GRANT ALTER ON ALL TABLES IN SCHEMA public TO your_migration_user;CREATE ON DATABASE lets EQL create its schemas (eql_v3, eql_v3_internal) and the public.* domain types; CREATE ON SCHEMA and ALTER are needed to add encrypted columns (typed as public.* domains, with their CHECK constraints) to your tables.
Runtime user — the application's day-to-day access:
-- EQL schema usage (resolves the encrypted operators / extractors)
GRANT USAGE ON SCHEMA eql_v3 TO your_app_user;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA eql_v3 TO your_app_user;
-- User table access (normal application permissions)
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE your_tables TO your_app_user;Schema changes — adding or removing encrypted columns — always go through the migration user.
Managed Postgres and Supabase
EQL v3 is designed to install without superuser. There are no custom operator classes (which managed platforms typically block), no postgresql.conf changes, and no separate Supabase build — the same schema installs everywhere. When the connected role lacks superuser, stash eql install detects it and automatically falls back to the no-operator-family install variant, so it works on Supabase, Neon, and RDS without extra flags. Indexing works through ordinary functional indexes over EQL's term-extractor functions, which any user who can CREATE INDEX can build. See the Supabase integration for platform-specific setup.
Understand
Core concepts
Domain variants, the encrypted payload, typed operands, and fail-loud blockers — the model every other page assumes.
Numbers
Encrypted integers, floats, and numerics.
Dates & times
Encrypted dates and timestamps: time windows, newest-first, retention cutoffs.
Text
Encrypted text: equality, ordering, and free-text token matching — and why there is no LIKE.
JSON
Encrypted JSON documents: containment, field access, and GIN indexing.
Booleans
Storage-only by design: why encrypted booleans carry no index terms.
Indexes
Functional-index recipes over the term extractors, and what it takes for an index to engage.
Use
Filtering
WHERE clauses on encrypted columns: equality, ranges, and text containment.
Sorting
ORDER BY on encrypted columns, and how to keep the sort in the index.
Grouping and aggregates
GROUP BY on the equality term, DISTINCT ON, COUNT, and the MIN / MAX aggregates.
Joins
Equijoins on encrypted columns and the same-keyset rule.
Provable access control
Bind decryption to an authenticated identity with lock contexts, so access to data is cryptographically demonstrable rather than merely logged.
Core concepts
The model behind every EQL page: domain variants that declare capability, the encrypted payload envelope, the typed-operand rule, and fail-loud blockers.