CipherStashDocs

Drizzle adapter reference

Encrypted query operators, schema extraction, EQL migration generation, and API surface for @cipherstash/stack/drizzle.

@cipherstash/stack/drizzle integrates CipherStash field-level encryption with Drizzle ORM. It provides a custom column type for encrypted fields and drop-in query operators that encrypt search values before they reach PostgreSQL. This page covers the operators, batching patterns, and migration generation. The step-by-step integration guide is at Drizzle integration guide. Full type signatures live in the auto-generated API reference.

Public entry points

ExportPurpose
encryptedTypeCustom Drizzle column type for an encrypted field. Accepts a dataType and index config.
extractEncryptionSchemaConverts a Drizzle pgTable definition into a CipherStash EncryptedTable schema for the SDK.
createEncryptionOperatorsReturns an object with all Drizzle query operators wrapped for encrypted columns.
EncryptedColumnConfigType alias for the column configuration object (dataType, equality, freeTextSearch, orderAndRange, searchableJson).
EncryptionConfigErrorThrown when a column lacks the index required by an operator.
EncryptionOperatorErrorThrown for operator-level failures (invalid arguments, unsupported operations).

Encrypted query operators

createEncryptionOperators returns a set of operators that mirror the standard Drizzle operator names. Each operator encrypts the search value before constructing the SQL fragment.

Key pattern: Most operators are async. await the operator call in the .where() clause, or pass un-awaited operators to encryptionOps.and() / encryptionOps.or() for batching.

OperatorEQL function / mechanismRequired column indexNotes
eq(col, value)PostgreSQL = on eql_v2_encryptedequality: trueAlso accepts orderAndRange: true
ne(col, value)PostgreSQL != on eql_v2_encryptedequality: trueAlso accepts orderAndRange: true
like(col, pattern)Bloom filter via eql_v2_encryptedfreeTextSearch: trueCase sensitivity depends on token filter config
ilike(col, pattern)Bloom filter via eql_v2_encryptedfreeTextSearch: trueCase sensitivity depends on token filter config
notIlike(col, pattern)Bloom filter via eql_v2_encryptedfreeTextSearch: true
gt(col, value)eql_v2.gt() ORE functionorderAndRange: true
gte(col, value)eql_v2.gte() ORE functionorderAndRange: true
lt(col, value)eql_v2.lt() ORE functionorderAndRange: true
lte(col, value)eql_v2.lte() ORE functionorderAndRange: true
between(col, min, max)eql_v2.gte() + eql_v2.lte()orderAndRange: trueInclusive
notBetween(col, min, max)ORE negationorderAndRange: true
inArray(col, values)Multiple equality encryptionsequality: true
notInArray(col, values)Multiple equality encryptionsequality: true
asc(col)ORE sort expressionorderAndRange: trueSync, no await needed
desc(col)ORE sort expressionorderAndRange: trueSync, no await needed
jsonbPathExists(col, path)eql_v2.jsonb_path_exists()searchableJson: trueReturns boolean for use in WHERE
jsonbPathQueryFirst(col, path)eql_v2.jsonb_path_query_first()searchableJson: trueReturns encrypted value for use in SELECT
jsonbGet(col, path)-> operator on eql_v2_encryptedsearchableJson: trueReturns encrypted value for use in SELECT

Non-encrypted columns fall back to the standard Drizzle operator automatically.

Sorting encrypted columns with asc() or desc() requires operator family support in the database. On managed databases (Supabase, RDS) or when EQL is installed with --exclude-operator-family, sort application-side after decrypting instead.

import { drizzle } from "drizzle-orm/postgres-js"
import { Encryption } from "@cipherstash/stack"
import { extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack/drizzle"
import { usersTable } from "./schema"
import postgres from "postgres"

const usersSchema = extractEncryptionSchema(usersTable)
const client = await Encryption({ schemas: [usersSchema] })
const ops = createEncryptionOperators(client)
const db = drizzle({ client: postgres(process.env.DATABASE_URL!) })

// Equality lookup
const exact = await db
  .select()
  .from(usersTable)
  .where(await ops.eq(usersTable.email, "[email protected]"))

// Range query on an encrypted number column
const adults = await db
  .select()
  .from(usersTable)
  .where(await ops.gte(usersTable.age, 18))

Batching conditions with and and or

Passing multiple operators to encryptionOps.and() or encryptionOps.or() batches all encryption into a single ZeroKMS call. This is more efficient than awaiting each operator separately.

Pass each operator without await as an argument to and() or or(), then await the outer call.

// All three encryptions happen in one ZeroKMS call
const results = await db
  .select()
  .from(usersTable)
  .where(
    await ops.and(
      ops.gte(usersTable.age, 18),
      ops.lte(usersTable.age, 65),
      ops.ilike(usersTable.email, "%@example.com"),
    ),
  )

Both and() and or() filter out undefined conditions, which makes conditional query building safe:

const results = await db
  .select()
  .from(usersTable)
  .where(
    await ops.and(
      searchEmail ? ops.ilike(usersTable.email, `%${searchEmail}%`) : undefined,
      ops.gte(usersTable.age, minAge),
    ),
  )

EQL migration generation

extractEncryptionSchema produces a CipherStash schema object from your Drizzle table. The @cipherstash/cli uses this schema to generate the EQL database migration that installs the required PostgreSQL indexes.

Run the migration generator after defining your table:

npx @cipherstash/cli db install

The CLI reads your Drizzle config and calls extractEncryptionSchema internally to determine which columns need EQL indexes. It then produces a timestamped SQL migration file in your Drizzle migrations directory.

See the CipherStash CLI reference for all db install options.

Full API surface

Everything else is in the auto-generated TypeDoc reference:

On this page