This is the Protect.js repository - End-to-end, per-value encryption for JavaScript/TypeScript with zero‑knowledge key management (via CipherStash ZeroKMS). Encrypted data is stored as EQL JSON payloads; searchable encryption is currently supported for PostgreSQL.
- Node.js: >= 22 (enforced in
package.jsonengines) - pnpm: 10.14.0 (this repo uses pnpm workspaces and catalogs)
- Internet access to install the prebuilt native module
@cipherstash/protect-ffi
If running integration tests or examples, you will also need CipherStash credentials (see Environment variables below).
pnpm installpnpm run build
# or only JS libraries
pnpm run build:jsUnder the hood this uses Turborepo to build ./packages/* with each package's tsup configuration.
pnpm run dev- Default: run package tests via Turborepo
pnpm test- Filter to a single package (recommended for fast iteration):
pnpm --filter @cipherstash/stack test
pnpm --filter @cipherstash/nextjs testTests use Vitest. Many tests talk to the real CipherStash service; they require environment variables. Some tests (e.g., lock context) are skipped if optional tokens aren't present.
Place these in a local .env at the repo root or specific example directory:
CS_WORKSPACE_CRN=
CS_CLIENT_ID=
CS_CLIENT_KEY=
CS_CLIENT_ACCESS_KEY=
# Optional – enables identity-aware encryption tests
USER_JWT=
USER_2_JWT=
# Logging (plaintext is never logged by design)
STASH_STACK_LOG=debug|info|error # default: error (errors only)If these variables are missing, tests that require live encryption will fail or be skipped; prefer filtering to specific packages and tests while developing.
packages/stack: Main package (@cipherstash/stack) containing the encryption client and all integrations- Subpath exports:
@cipherstash/stack,@cipherstash/stack/schema,@cipherstash/stack/identity,@cipherstash/stack/secrets,@cipherstash/stack/drizzle,@cipherstash/stack/supabase,@cipherstash/stack/dynamodb,@cipherstash/stack/client,@cipherstash/stack/types
- Subpath exports:
packages/protect: Core encryption library (internal, re-exported via@cipherstash/stack)src/index.ts: Public API (Encryption, exports)src/ffi/index.ts:EncryptionClientimplementation, bridges to@cipherstash/protect-ffisrc/ffi/operations/*: Encrypt/decrypt/model/bulk/query operations (thenable pattern with optional.withLockContext())__tests__/*: End-to-end and API contract tests (Vitest)
packages/schema: Schema builder utilities and types (encryptedTable,encryptedColumn,encryptedField)packages/drizzle: Drizzle ORM integration (encryptedType,extractEncryptionSchema,createEncryptionOperators)packages/nextjs: Next.js helpers and Clerk integration (./clerkexport)packages/protect-dynamodb: DynamoDB helpers (encryptedDynamoDB)packages/utils: Shared config (utils/config) and logger (utils/logger)examples/*: Working apps (basic, drizzle, nextjs-clerk, next-drizzle-mysql, dynamo, hono-supabase)docs/*: Concepts, how-to guides (Next.js bundling, SST, npm lockfile v3), referenceskills/*: Agent skills (stash-encryption,stash-drizzle,stash-dynamodb,stash-secrets,stash-supabase)
- Initialization:
Encryption({ schemas })returns an initializedEncryptionClient. Provide at least oneencryptedTable. - Schema: Define tables/columns with
encryptedTableandencryptedColumnfrom@cipherstash/stack/schema. Add.freeTextSearch().equality().orderAndRange()to enable searchable encryption on PostgreSQL. Use.searchableJson()for encrypted JSONB queries. UseencryptedFieldfor nested object encryption (DynamoDB). - Operations (all return Result-like objects and support chaining
.withLockContext(lockContext)and.audit()when applicable):encrypt(plaintext, { table, column })decrypt(encryptedPayload)encryptModel(model, table)/decryptModel(model)bulkEncrypt(plaintexts[], { table, column })/bulkDecrypt(encrypted[])bulkEncryptModels(models[], table)/bulkDecryptModels(models[])encryptQuery(value, { table, column, queryType?, returnType? })for searchable queriesencryptQuery(terms[])for batch query encryption
- Identity-aware encryption: Use
LockContextfrom@cipherstash/stack/identityand chain.withLockContext()on operations. Same context must be used for both encrypt and decrypt. - Integrations:
- Drizzle ORM:
encryptedType,extractEncryptionSchema,createEncryptionOperatorsfrom@cipherstash/stack/drizzle - Supabase:
encryptedSupabasefrom@cipherstash/stack/supabase - DynamoDB:
encryptedDynamoDBfrom@cipherstash/stack/dynamodb
- Drizzle ORM:
- Secrets management:
Secretsclass from@cipherstash/stack/secretsfor encrypted secret storage and retrieval.
- Native Node.js module:
@cipherstash/stackrelies on@cipherstash/protect-ffi(Node-API). It must be loaded via native Node.jsrequire. Do NOT bundle this module; configure bundlers to externalize it.- Next.js: see
docs/how-to/nextjs-external-packages.md - SST/Serverless: see
docs/how-to/sst-external-packages.md - npm lockfile v3 on Linux: see
docs/how-to/npm-lockfile-v3.md
- Next.js: see
- Do not log plaintext: The library never logs plaintext by design. Don't add logs that risk leaking sensitive data.
- Result shape is contract: Operations return
{ data }or{ failure }. Preserve this shape and errortypevalues inEncryptionErrorTypes. - Encrypted payload shape is contract: Keys like
cin the EQL payload are validated by tests and downstream tools. Don't change them. - Exports must support ESM and CJS: Each package's
exportsmaps must keep bothimportandrequirefields. Don't remove CJS.
- Formatting/Linting: Use Biome
pnpm run code:fix- Build:
pnpm run build(Turborepo + tsup per package) - Test:
pnpm --filter <pkg> testfor targeted iterations - Releases: Use Changesets
pnpm changeset # create a changeset
pnpm changeset:version
pnpm changeset:publish- Use Vitest with
.test.tsfiles under each package's__tests__/. - Import
dotenv/configat the top when tests need environment variables. - Prefer testing via the public API. Avoid reaching into private internals.
- Some tests have larger timeouts (e.g., 30s) to accommodate network calls.
- When integrating into frameworks/build tools, ensure native modules are externalized and loaded via Node's runtime require.
- For Next.js, configure
serverExternalPackagesas documented indocs/how-to/nextjs-external-packages.md. - For serverless/Linux targets with npm lockfile v3, see
docs/how-to/npm-lockfile-v3.mdto avoid runtime load errors.
- Identify the target package(s) in
packages/*and confirm whether changes affect public APIs or payload shapes. - If modifying
packages/protectoperations orEncryptionClient, ensure:- The Result contract and error type strings remain stable.
.withLockContext()remains available for affected operations.- ESM/CJS exports continue to work (don't break
require).
- If changing schema behavior (
packages/schema), update type definitions and ensure validation still works inEncryptionClient.init. - Add/extend tests in the same package. For features that require live credentials, guard with env checks or provide mock-friendly paths.
- Run:
pnpm run code:fixpnpm --filter <changed-pkg> buildpnpm --filter <changed-pkg> test
- Update docs in
docs/*and usage examples if APIs change.
README.mdfor quickstart and feature overviewdocs/concepts/searchable-encryption.mddocs/concepts/aws-kms-vs-cipherstash-comparison.mddocs/reference/schema.mddocs/reference/searchable-encryption-postgres.mddocs/reference/configuration.mddocs/reference/identity.mddocs/reference/secrets.mddocs/reference/dynamodb.mddocs/reference/supabase-sdk.mddocs/reference/drizzle/drizzle.mddocs/how-to/nextjs-external-packages.mddocs/how-to/sst-external-packages.mddocs/how-to/npm-lockfile-v3.md
- Module load errors on Linux/serverless: review the npm lockfile v3 guide.
- Can't decrypt after encrypting with a lock context: ensure the exact same lock context is provided to decrypt.
- Tests failing due to missing credentials: provide
CS_*env vars; lock-context tests are skipped withoutUSER_JWT. - Performance testing: prefer bulk operations (
bulkEncrypt*/bulkDecrypt*) to exercise ZeroKMS bulk speed.