You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@cipherstash/stack/prisma — Prisma Next integration
Adds a new @cipherstash/stack/prisma extension pack that wires CipherStash's searchable
application-layer field-level encryption into Prisma
Next applications. Plaintext goes in, ciphertext
lands in Postgres, queries against encrypted columns work natively. Single extension pack,
no middleware, no manual bulkEncrypt / bulkDecrypt calls.
The integration is enabled by Prisma's single-path async codec runtime (#379, ADR
204) — encryption lives directly inside codec.encode / codec.decode rather than in a middleware-shaped workaround.
What's in the pack
5 codecs — one storage codec (cs/eql_v2_encrypted@1) plus four query-term codecs
(eq_term, match_term, ore_term, ste_vec_selector). Each operator's SqlOperationDescriptor pins the right value-side codec via per-arg codecId, so the same
plaintext encrypts as the right shape (eq vs match vs ORE vs STE-vec selector) per
operator.
Conditional OperationTypes — .eq() only surfaces on equality: true columns, .gte() only on orderAndRange: true, .ilike() only on freeTextSearch: true, etc.
Argument types match the column's dataType (Date for encryptedDate, number for encryptedNumber, the user's T for encryptedJson<T>).
Microtask batcher — coalesces all in-flight encode / decode calls within a single Promise.all dispatch into one ZeroKMS round-trip per encodeParams / decodeRow
invocation.
Migration support — vendored EQL install bundle (pinned to eql-2.2.1) ships via databaseDependencies.init; per-column index DDL emits via planTypeOperations. pnpm prisma-next db migrate does the right thing.
Multi-tenancy — per-extension EncryptionClient binding (no module-level singleton).
Construct one extension per tenant scope; each carries its own keyset.
Observability — cipherstashEncryption({ onEvent }) callback fires for every ZeroKMS
round-trip with { kind, codecId, batchSize, durationMs, table?, column?, error? }.
Default behavior is silent in production, console.debug in dev.
Eager env validation — required env vars (CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY) validated at extension construction with structured CipherStashCodecError (code CONFIG_MISSING_ENV).
Decrypted<Contract, Model> type helper — walks the contract, narrows encrypted
fields to their decrypted JS types, propagates T through encryptedJson<T>.
114 unit tests passing across 12 files (prisma-batcher, prisma-codec, prisma-codec-errors, prisma-column-types, prisma-control, prisma-database-dependencies, prisma-decrypted, prisma-extraction, prisma-multi-tenancy, prisma-observability, prisma-operation-templates, prisma-runtime).
6 env-gated Postgres + ZeroKMS integration tests that exercise insert / equality /
range / free-text / Date round-trip / STE-Vec selector against a real database when DATABASE_URL + CS_* are set.
Multi-tenancy test asserts two extensions with two mock clients show no cross-talk.
Microtask-batching tests assert N-row inserts produce a single bulkEncrypt call, not N.
Full repo suite stays green: 567 passed / 9 skipped.
What's not yet supported
Documented in packages/stack/src/prisma/README.md under "What's not yet supported":
Feature
Why deferred
Shorthand where({ email: '…' }) on encrypted columns
Prisma Next inlines literals;
pending an upstream preferParam codec trait. Use the fluent .eq(param(…)) form.
inArray on encrypted columns
No eql_v2.in_array SQL function exists in EQL. Compose
Prisma Next does not currently plumb column metadata to per-call codec.encode / codec.decode. The integration works around this by dispatching by JS-runtime data type to
the first contract column matching that data type. For contracts with multiple encrypted
columns of the same data type, every value of that data type encrypts under the first
matching column's index configuration. The right long-term fix is upstream — either
Prisma Next plumbs column context to per-call codec methods, or we adopt a
one-codec-instance-per-column registration pattern via the parameterized-codec init(typeParams) hook.
Documented in JSDoc on core/encryption-client.ts, in the README's "What's not yet
supported", and in the v2 plan's cleanup decisions log. Surfaced as NO_COLUMN_FOR_DATATYPE errors when no column matches.
Documentation
packages/stack/src/prisma/README.md — usage source of truth (~315 lines): install,
setup, contract authoring, all five usage patterns (insert / equality / range / free-text /
JSONB), multi-tenancy, observability, errors.
notes/cipherstash-prisma-integration-plan-v2.md — architectural reference, including
the cleanup decisions log capturing every meaningful trade-off (LockContext deferred, flat
operators chosen, per-extension client, SDK trusted for payload round-trip).
notes/cipherstash-prisma-dx-audit.md — 32-finding DX audit that drove the cleanup pass.
Root README.md and packages/stack/README.md updated with consistent data-level access
control positioning.
How to test locally
pnpm --filter @cipherstash/stack build
pnpm --filter @cipherstash/stack exec vitest run __tests__/prisma-*.test.ts
# Optional — Postgres + ZeroKMS integration:
DATABASE_URL=postgres://... CS_WORKSPACE_CRN=... CS_CLIENT_ID=... CS_CLIENT_KEY=... \
pnpm --filter @cipherstash/stack exec vitest run __tests__/prisma-codec-pg.test.ts
EQL must be installed in the target Postgres instance for the integration tests to pass;
the install SQL ships with the package at packages/stack/src/prisma/core/eql-install.sql
(regenerable via pnpm --filter @cipherstash/stack vendor-eql-install).
Notes for reviewers
- The vendored Prisma Next types in packages/stack/src/prisma/internal-types/prisma-next.ts
exist because Prisma Next is pre-publish on npm. They mirror the post-#379 surface and
will be replaced with @prisma-next/* peer dependencies (all optional: true) when Prisma
Next ships.
- Two minor SDK-API deviations from the v2 plan: (1) env vars are CS_WORKSPACE_CRN /
CS_CLIENT_ID / CS_CLIENT_KEY (not CS_CLIENT_KEY_ID); (2) bulkDecrypt(payloads) doesn't take (table, column) — the cipher's own i.t / i.c markers drive FFI dispatch internally.
Documented inline.
- The EQL install bundle (eql-install.sql, eql-install.generated.ts) is large (~5.7K lines)
but committed deliberately so dev environments and offline builds work without network.
Regeneration is idempotent.
- No changes to packages/stack/src/encryption/, packages/stack/src/identity/, or any other
existing integration (drizzle, supabase, dynamodb, schema).
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets
When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types
Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e1e1a7f5-0e72-41a4-8eea-5f4304d6ad94
You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.
Use the checkbox below for a quick retry:
🔍 Trigger review
✨ Finishing Touches🧪 Generate unit tests (beta)
Create PR with unit tests
Commit unit tests in branch prisma-next
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@cipherstash/stack/prisma— Prisma Next integrationAdds a new
@cipherstash/stack/prismaextension pack that wires CipherStash's searchableapplication-layer field-level encryption into Prisma
Next applications. Plaintext goes in, ciphertext
lands in Postgres, queries against encrypted columns work natively. Single extension pack,
no middleware, no manual
bulkEncrypt/bulkDecryptcalls.The integration is enabled by Prisma's single-path async codec runtime (#379, ADR
204) — encryption lives directly inside
codec.encode/codec.decoderather than in a middleware-shaped workaround.What's in the pack
cs/eql_v2_encrypted@1) plus four query-term codecs(
eq_term,match_term,ore_term,ste_vec_selector). Each operator'sSqlOperationDescriptorpins the right value-side codec via per-argcodecId, so the sameplaintext encrypts as the right shape (eq vs match vs ORE vs STE-vec selector) per
operator.
encryptedString,encryptedNumber,encryptedDate,encryptedBoolean,encryptedJson<T>with searchable-encryption configuration(
equality,freeTextSearch,orderAndRange,searchableJson).eq,ne,gt,gte,lt,lte,between,notBetween,like,ilike,notIlike,jsonbPathExists,jsonbPathQueryFirst,jsonbGet. SQL templates verified against the existing Drizzle integration.OperationTypes—.eq()only surfaces onequality: truecolumns,.gte()only onorderAndRange: true,.ilike()only onfreeTextSearch: true, etc.Argument types match the column's
dataType(DateforencryptedDate,numberforencryptedNumber, the user'sTforencryptedJson<T>).encode/decodecalls within a singlePromise.alldispatch into one ZeroKMS round-trip perencodeParams/decodeRowinvocation.
eql-2.2.1) ships viadatabaseDependencies.init; per-column index DDL emits viaplanTypeOperations.pnpm prisma-next db migratedoes the right thing.EncryptionClientbinding (no module-level singleton).Construct one extension per tenant scope; each carries its own keyset.
cipherstashEncryption({ onEvent })callback fires for every ZeroKMSround-trip with
{ kind, codecId, batchSize, durationMs, table?, column?, error? }.Default behavior is silent in production,
console.debugin dev.CS_WORKSPACE_CRN,CS_CLIENT_ID,CS_CLIENT_KEY) validated at extension construction with structuredCipherStashCodecError(codeCONFIG_MISSING_ENV).Decrypted<Contract, Model>type helper — walks the contract, narrows encryptedfields to their decrypted JS types, propagates
TthroughencryptedJson<T>.CipherStashCodecErrorwith codesJS_TYPE_MISMATCH,UNSUPPORTED_PLAINTEXT_TYPE,INVALID_QUERY_TERM,DECODE_ROUND_TRIP_BROKEN,NO_COLUMN_FOR_DATATYPE,CONFIG_MISSING_ENV,NO_CONTRACT_SCHEMAS.Subpath exports
@cipherstash/stack/prisma/controlprisma-next.config.ts)@cipherstash/stack/prisma/runtimedb.ts)@cipherstash/stack/prisma/packcontract.ts)@cipherstash/stack/prisma/column-typesencryptedString/encryptedNumber/ etc.@cipherstash/stack/prisma/codec-typesDecrypted<Contract, Model>,JsTypeFor,CodecTypes@cipherstash/stack/prisma/operation-typesOperationTypesfor the@cipherstash/stack/prismaTests
prisma-batcher,prisma-codec,prisma-codec-errors,prisma-column-types,prisma-control,prisma-database-dependencies,prisma-decrypted,prisma-extraction,prisma-multi-tenancy,prisma-observability,prisma-operation-templates,prisma-runtime).range / free-text / Date round-trip / STE-Vec selector against a real database when
DATABASE_URL+CS_*are set.bulkEncryptcall, not N.What's not yet supported
Documented in
packages/stack/src/prisma/README.mdunder "What's not yet supported":where({ email: '…' })on encrypted columnspreferParamcodec trait. Use the fluent.eq(param(…))form.inArrayon encrypted columnseql_v2.in_arraySQL function exists in EQL. Composeor(...)of.eq()calls..order()/.asc()/.desc()on ORE columnsLockContext)Known upstream limitation
Prisma Next does not currently plumb column metadata to per-call
codec.encode/codec.decode. The integration works around this by dispatching by JS-runtime data type tothe first contract column matching that data type. For contracts with multiple encrypted
columns of the same data type, every value of that data type encrypts under the first
matching column's index configuration. The right long-term fix is upstream — either
Prisma Next plumbs column context to per-call codec methods, or we adopt a
one-codec-instance-per-column registration pattern via the parameterized-codec
init(typeParams)hook.Documented in JSDoc on
core/encryption-client.ts, in the README's "What's not yetsupported", and in the v2 plan's cleanup decisions log. Surfaced as
NO_COLUMN_FOR_DATATYPEerrors when no column matches.Documentation
packages/stack/src/prisma/README.md— usage source of truth (~315 lines): install,setup, contract authoring, all five usage patterns (insert / equality / range / free-text /
JSONB), multi-tenancy, observability, errors.
notes/cipherstash-prisma-integration-plan-v2.md— architectural reference, includingthe cleanup decisions log capturing every meaningful trade-off (LockContext deferred, flat
operators chosen, per-extension client, SDK trusted for payload round-trip).
notes/cipherstash-prisma-dx-audit.md— 32-finding DX audit that drove the cleanup pass.README.mdandpackages/stack/README.mdupdated with consistent data-level accesscontrol positioning.
How to test locally