Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/prisma/prisma-next/llms.txt

Use this file to discover all available pages before exploring further.

The CipherStash extension pack adds six encrypted column types and 17 query operators to Prisma Next, backed by the CipherStash EQL bundle. Columns are encrypted at the application layer before they reach the database, yet remain fully queryable — equality, range, substring search, and JSON path operators all work against ciphertext through EQL’s searchable encryption indices.

What the extension provides

  • Six encrypted column types: EncryptedString, EncryptedDouble, EncryptedBigInt, EncryptedDate, EncryptedBoolean, EncryptedJson. Each maps a plain JS type to an EQL ciphertext stored in a single eql_v2_encrypted Postgres column.
  • Per-codec search-mode flags (equality, freeTextSearch, orderAndRange, searchableJson) that drive which EQL search-config indices are emitted at migration time. All flags default to true.
  • 13 predicate operators surfaced as column methods (e.g. m.email.cipherstashEq(...), m.salary.cipherstashGt(...)).
  • 4 free-standing helpers for sort and JSON projection: cipherstashAsc, cipherstashDesc, cipherstashJsonbPathQueryFirst, cipherstashJsonbGet.
  • bulkEncryptMiddleware(sdk) — coalesces cipherstash parameters across rows into one bulkEncrypt SDK round-trip per (table, column) group before wire encoding.
  • decryptAll(rows) — reads a result set and coalesces every cipherstash envelope into one bulkDecrypt SDK round-trip per (table, column) group.
  • EQL contract space — the extension ships the eql_v2_configuration table, eql_v2_encrypted composite types, eql_v2 domains, and the EQL bundle SQL as a Prisma Next contract space so the framework manages it alongside your own schema.

The six encrypted column types

TS factory / PSL constructorJS plaintextEQL cast_asSearch-mode flags
encryptedString / cipherstash.EncryptedStringstringtextequality, freeTextSearch, orderAndRange
encryptedDouble / cipherstash.EncryptedDoublenumber (IEEE-754)doubleequality, orderAndRange
encryptedBigInt / cipherstash.EncryptedBigIntbigintbig_intequality, orderAndRange
encryptedDate / cipherstash.EncryptedDateDate (calendar date)dateequality, orderAndRange
encryptedBoolean / cipherstash.EncryptedBooleanbooleanbooleanequality
encryptedJson / cipherstash.EncryptedJsonJSON-serialisable unknownjsonbsearchableJson
Each enabled flag maps to one EQL search-config index. The migration lifecycle hook emits eql_v2.add_search_config(...) operations at field-added events and eql_v2.remove_search_config(...) operations when a flag is turned off between contract versions.

Setup

1

Install the package

pnpm add @prisma-next/extension-cipherstash
2

Register the extension in prisma-next.config.ts

prisma-next.config.ts
import { defineConfig } from '@prisma-next/cli/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import sql from '@prisma-next/family-sql/control';
import postgres from '@prisma-next/target-postgres/control';
import cipherstash from '@prisma-next/extension-cipherstash/control';

export default defineConfig({
  family: sql,
  target: postgres,
  adapter: postgresAdapter,
  extensionPacks: [cipherstash],
});
3

Declare encrypted columns in your schema

model User {
  id              Int @id @default(autoincrement())

  email           cipherstash.EncryptedString({ orderAndRange: true })
  searchableEmail cipherstash.EncryptedString({ freeTextSearch: true })
  salary          cipherstash.EncryptedDouble()
  accountId       cipherstash.EncryptedBigInt() @map("accountid")
  birthday        cipherstash.EncryptedDate()
  emailVerified   cipherstash.EncryptedBoolean() @map("emailverified")
  profile         cipherstash.EncryptedJson()
}
Search-mode flags default to true. To opt out of a specific index, set the flag to false explicitly — for example, encryptedString({ equality: false, freeTextSearch: false, orderAndRange: false }) for storage-only encryption with no queryable indices.
4

Emit the contract and apply migrations

pnpm prisma-next contract emit
pnpm prisma-next db init
db init applies the bundled EQL baseline migration, which installs the eql_v2_configuration table, eql_v2_encrypted type, eql_v2 domains, and the EQL bundle SQL before your application migrations run.
5

Wire the extension and middleware into the runtime

src/prisma/db.ts
import { bulkEncryptMiddleware } from '@prisma-next/extension-cipherstash/middleware';
import { createCipherstashRuntimeDescriptor } from '@prisma-next/extension-cipherstash/runtime';
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from './prisma/contract.d';
import contractJson from './prisma/contract.json' with { type: 'json' };

const sdk = /* your CipherstashSdk implementation */;

export const db = postgres<Contract>({
  contractJson,
  extensions: [createCipherstashRuntimeDescriptor({ sdk })],
  middleware: [bulkEncryptMiddleware(sdk)],
});

Writing and reading encrypted data

import {
  cipherstashAsc,
  decryptAll,
  EncryptedBigInt,
  EncryptedDate,
  EncryptedDouble,
  EncryptedString,
} from '@prisma-next/extension-cipherstash/runtime';
import { and } from '@prisma-next/sql-orm-client';
import { db } from './prisma/db';

// Write — bulkEncryptMiddleware coalesces columns into one bulkEncrypt call per (table, column) group.
await db.orm.User.create({
  id: 1,
  email: EncryptedString.from('alice@example.com'),
  salary: EncryptedDouble.from(75_000.50),
  accountId: EncryptedBigInt.from(1_000_000_000_001n),
  birthday: EncryptedDate.from(new Date('1985-03-15')),
});

// Read — predicate operators on column accessors; cipherstashAsc as a free-standing helper.
const rows = await db.orm.User
  .where((u) => and(
    u.email.cipherstashIlike('%@example.com'),
    u.salary.cipherstashGt(50_000),
    u.birthday.cipherstashLt(new Date('1990-01-01')),
  ))
  .orderBy((u) => [cipherstashAsc(u.salary)])
  .all();

// Decrypt — one bulkDecrypt per (table, column) group.
await decryptAll(rows);
console.log(await rows[0]?.email.decrypt());

Operator reference

Predicate operators (column methods)

Predicate operators return boolean and are available in .where() clauses. Each operator requires a specific search-mode flag to be enabled on the column codec.
OperatorRequired flagLoweringApplies to
cipherstashEq(plaintext)equalityeql_v2.eq(self, $N)all codecs
cipherstashNe(plaintext)equalityNOT eql_v2.eq(self, $N)all codecs
cipherstashInArray([...])equalityeql_v2.eq(self, $1) OR eql_v2.eq(self, $2) OR ...all codecs
cipherstashNotInArray([...])equalityNOT (eql_v2.eq(self, $1) OR ...)all codecs
cipherstashIlike(pattern)freeTextSearcheql_v2.ilike(self, $N)EncryptedString
cipherstashNotIlike(pattern)freeTextSearchNOT eql_v2.ilike(self, $N)EncryptedString
cipherstashGt(plaintext)orderAndRangeeql_v2.gt(self, $N)EncryptedString, EncryptedDouble, EncryptedBigInt, EncryptedDate
cipherstashGte(plaintext)orderAndRangeeql_v2.gte(self, $N)as above
cipherstashLt(plaintext)orderAndRangeeql_v2.lt(self, $N)as above
cipherstashLte(plaintext)orderAndRangeeql_v2.lte(self, $N)as above
cipherstashBetween(lo, hi)orderAndRangeeql_v2.gte(self, $1) AND eql_v2.lte(self, $2)as above
cipherstashNotBetween(lo, hi)orderAndRangeNOT (eql_v2.gte(...) AND eql_v2.lte(...))as above
cipherstashJsonbPathExists(path)searchableJsoneql_v2.jsonb_path_exists(self, $N)EncryptedJson
The framework’s built-in operators (eq, gt, ilike, etc.) are not available on cipherstash columns and will produce a compile-time error. EQL ciphertexts contain randomized nonces — SQL = or < against raw eql_v2_encrypted values always returns false. The cipherstash-namespaced operators route through the correct EQL search-config indices.

Free-standing helpers (non-predicate)

These helpers take a column expression as input and return a sort item or a typed expression — they are not assignable to the column-method predicate surface.
import {
  cipherstashAsc,
  cipherstashDesc,
  cipherstashJsonbPathQueryFirst,
  cipherstashJsonbGet,
} from '@prisma-next/extension-cipherstash/runtime';
HelperRequired flagReturnsApplies to
cipherstashAsc(col)orderAndRangeOrderByItemEncryptedString, EncryptedDouble, EncryptedBigInt, EncryptedDate
cipherstashDesc(col)orderAndRangeOrderByItemas above
cipherstashJsonbPathQueryFirst(col, path)searchableJsonExpression<cipherstash/json@1>EncryptedJson
cipherstashJsonbGet(col, path)searchableJsonExpression<cipherstash/json@1>EncryptedJson
The JSON helpers return the same type as their input column, so they chain into follow-on JSON helpers or predicates.

EQL search-config index types

Each search-mode flag maps to one EQL index family:
EQL indexTriggered byWhat it enables
uniqueequalityDeterministic lookup via eql_v2.eq / eql_v2.in_array.
matchfreeTextSearchBloom-filter substring search via eql_v2.ilike. Probabilistic — false positives possible, false negatives not.
oreorderAndRangeOrder-revealing encryption for eql_v2.gt / gte / lt / lte / between and bare-column ORDER BY.
ste_vecsearchableJsonSearchable tree encoding for eql_v2.jsonb_path_query_first and eql_v2."->".

Choosing the right codec

You want to …Pick
Searchable email or arbitrary string with substring searchencryptedString({ equality: true, freeTextSearch: true })
Numeric range queries on salary, price, or scoreencryptedDouble({ equality: true, orderAndRange: true })
Account or ID number with exact-match and rangeencryptedBigInt({ equality: true, orderAndRange: true })
Calendar-date range queriesencryptedDate({ equality: true, orderAndRange: true })
Boolean flag with WHERE col = true predicatesencryptedBoolean({ equality: true })
Searchable JSON documentencryptedJson({ searchableJson: true })
Storage-only encryption (no queryable indices)Any factory with every flag set to false

Subpath exports

SubpathPurpose
./controlSqlControlExtensionDescriptor (contract space, pack metadata, codec lifecycle hooks)
./runtimeEnvelope classes, CipherstashSdk, codec runtime, decryptAll, free-standing helpers
./middlewarebulkEncryptMiddleware(sdk)
./packcipherstashPackMeta for TypeScript contract authoring
./column-typesSix TS factories: encryptedString, encryptedDouble, encryptedBigInt, encryptedDate, encryptedBoolean, encryptedJson
The ./control, ./runtime, and ./middleware planes are tree-shakable — a runtime consumer never pulls the EQL bundle SQL or codec lifecycle hooks, and a control-plane consumer never pulls envelope classes, the SDK interface, or the middleware.

Security model

  • Plaintext lifetime — the write-side envelope retains its plaintext slot post-encrypt. Treat envelope objects as plaintext-equivalent for the lifetime of the variable.
  • Ciphertext routing — every read-side envelope carries the (table, column) it was decoded from. decrypt and decryptAll route bulk SDK calls by that key so the SDK can select the correct key material per column.
  • Plaintext redactiontoJSON, toString, valueOf, Symbol.toPrimitive, and Symbol.for('nodejs.util.inspect.custom') all return [REDACTED]. Accidental console.log, JSON.stringify, template-literal interpolation, and util.inspect cannot leak plaintext. Explicit access is via envelope.expose().
  • Cancellation — every internal SDK call accepts an AbortSignal.

Known limitations

The following are explicitly deferred from the current implementation.
  • cipherstashJsonbPathExists against the live EQL bundle — the framework currently binds the JSONpath as a plain text ParamRef rather than the hashed selector the EQL bundle expects. Predicate queries return zero rows against rows that should match. The non-predicate helpers (cipherstashJsonbPathQueryFirst, cipherstashJsonbGet) work correctly. Workaround: project all rows with the SELECT-expression helpers and filter client-side.
  • EncryptedBigInt capped at Number.MAX_SAFE_INTEGER — the CipherStash SDK’s JsPlaintext union does not include bigint. Values beyond Number.MAX_SAFE_INTEGER cannot be encrypted and will throw on overflow.
  • No encrypted timestamp / datetime — CipherStash offers only calendar-date encryption (EncryptedDate). Encrypted timestamp support is deferred.
  • No re-encryption migration primitive — adopting cipherstash for an existing populated column requires re-encrypting row data. The codec lifecycle hook emits the correct search-config DDL but does not touch row data. Work around it with a hand-authored dataTransform migration.

Build docs developers (and LLMs) love