Skip to content

feat(stack): init prisma next integration#364

Draft
calvinbrewer wants to merge 1 commit intomainfrom
prisma-next
Draft

feat(stack): init prisma next integration#364
calvinbrewer wants to merge 1 commit intomainfrom
prisma-next

Conversation

@calvinbrewer
Copy link
Copy Markdown
Contributor

@calvinbrewer calvinbrewer commented Apr 27, 2026

@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.
  • 5 column-type factoriesencryptedString, encryptedNumber, encryptedDate,
    encryptedBoolean, encryptedJson<T> with searchable-encryption configuration
    (equality, freeTextSearch, orderAndRange, searchableJson).
  • 14 operator descriptorseq, ne, gt, gte, lt, lte, between,
    notBetween, like, ilike, notIlike, jsonbPathExists, jsonbPathQueryFirst,
    jsonbGet. SQL templates verified against the existing Drizzle integration.
  • 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.
  • ObservabilitycipherstashEncryption({ 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>.
  • Structured errorsCipherStashCodecError with codes JS_TYPE_MISMATCH,
    UNSUPPORTED_PLAINTEXT_TYPE, INVALID_QUERY_TERM, DECODE_ROUND_TRIP_BROKEN,
    NO_COLUMN_FOR_DATATYPE, CONFIG_MISSING_ENV, NO_CONTRACT_SCHEMAS.

Subpath exports

Path Use
@cipherstash/stack/prisma/control Build-time / migration planner (used in
prisma-next.config.ts)
@cipherstash/stack/prisma/runtime Runtime extension (used in db.ts)
@cipherstash/stack/prisma/pack Pack metadata (used in contract.ts)
@cipherstash/stack/prisma/column-types encryptedString / encryptedNumber / etc.
@cipherstash/stack/prisma/codec-types Type-only: Decrypted<Contract, Model>,
JsTypeFor, CodecTypes
@cipherstash/stack/prisma/operation-types Type-only: OperationTypes for the
contract emitter
@cipherstash/stack/prisma Convenience barrel

Tests

  • 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
or(...) of .eq() calls.
.order() / .asc() / .desc() on ORE columns Prisma Next's fluent column-side
ordering surface is unstable post-#379.
Identity-aware encryption (LockContext) Returning when decryption policies land
server-side.
Cross-row decode batching Pending upstream
TML-2330. Within-row batching is
shipped.

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 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).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 27, 2026

⚠️ No Changeset found

Latest commit: 8e8e5a2

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

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Draft detected.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant