npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cipherstash/stack-drizzle

v1.1.1

Published

CipherStash Stack Drizzle ORM integration: searchable, application-layer field-level encryption for PostgreSQL.

Readme

Why

Anyone with database access — a DBA, a leaked service account, a SQL injection — normally sees everything. CipherStash encrypts each value with its own key, derived at query time from the caller's identity. So a dump, an injection, or a compromised box yields ciphertext; you can only decrypt what you're explicitly authorized to, and every decryption is audited.

The trick is queries still work: we build searchable encrypted indexes using deterministic encryption, ORE, and bloom filters, so equality, range, and fuzzy-text queries run against native Postgres indexes in milliseconds without decrypting the table.

The trade-off is explicit and bounded: the indexes leak equality and order relationships, nothing else — it's not FHE, and we don't pretend it is. Security architecture →

Encrypted columns. Real Drizzle queries.

The email and age columns below are stored as ciphertext with a unique key per row — and the queries still work, because they run on the ciphertext. No decrypt-and-scan, no query rewriting layer, no proxy in the query path.

export const users = pgTable('users', {
  id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
  email: types.TextSearch('email'), // → eql_v3_text_search — the type is the config
  age: types.IntegerOrd('age'),     // → eql_v3_integer_ord
})

const rows = await db
  .select()
  .from(users)
  .where(await ops.and(
    ops.matches(users.email, 'alice'),  // free-text match, on ciphertext
    ops.between(users.age, 18, 65),     // range, on ciphertext
  ))
  .orderBy(ops.asc(users.age))          // ordered by encrypted value

The operators mirror Drizzle's and encrypt their operands transparently:

| Query type | Operators | Docs | |---|---|---| | Equality | ops.eq, ops.ne, ops.inArray | Equality queries → | | Range & ordering | ops.gt/gte/lt/lte, ops.between, ops.asc/desc | Range & ordering → | | Free-text match | ops.matches | Text search → | | Encrypted JSON | ops.contains (containment), ops.selector(col, path) | JSON → |

Each column's query capabilities are fixed by its type, so an unsupported operation is rejected loudly instead of silently scanning.

Quick start

About five minutes, starting on the free developer tier (sign up). The setup wizard handles authentication, the EQL install, and your schema:

npx stash init

Or install manually (this package depends on @cipherstash/stack; install both), then run stash eql install once — or generate a migration with stash eql migration --drizzle:

npm install @cipherstash/stack @cipherstash/stack-drizzle drizzle-orm

Full guide: Drizzle quickstart →

Full example (EQL v3)

Each encrypted column is a concrete public.eql_v3_* Postgres domain whose query capabilities are fixed by the types.* factory you choose — no per-column config object:

import { pgTable, integer } from 'drizzle-orm/pg-core'
import { Encryption } from '@cipherstash/stack/v3'
import {
  types,
  extractEncryptionSchema,
  createEncryptionOperators,
} from '@cipherstash/stack-drizzle'

const users = pgTable('users', {
  id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
  email: types.TextSearch('email'), // equality + order/range + free-text
  age: types.IntegerOrd('age'),     // equality + order/range
})

const schema = extractEncryptionSchema(users)
const client = await Encryption({ schemas: [schema] })
const ops = createEncryptionOperators(client)

// Insert — encrypt models first (bulk helpers batch key operations
// through ZeroKMS instead of one round trip per row)
const enc = await client.bulkEncryptModels(
  [{ email: '[email protected]', age: 30 }],
  schema,
)
if (!enc.failure) await db.insert(users).values(enc.data)

// Query — operators auto-encrypt their plaintext operands
const rows = await db
  .select()
  .from(users)
  .where(await ops.and(
    ops.matches(users.email, 'alice'), // free-text token match over ciphertext
    ops.between(users.age, 18, 65),
  ))
  .orderBy(ops.asc(users.age))

// Decrypt after select
const dec = await client.bulkDecryptModels(rows, schema)

For a types.Json column, ops.selector(column, path) supports encrypted comparisons and ordering at a scalar JSONPath leaf. For example, .orderBy(await ops.selector(users.profile, '$.age').asc()) lowers to ORDER BY eql_v3.ord_term(...) over the selected encrypted entry.

Indexing encrypted columns

Encrypted predicates only use an index if one exists over the matching eql_v3.* term-extractor expression — otherwise every encrypted query sequential-scans. encryptedIndexes derives the recommended indexes for every encrypted column in a table; spread it into pgTable's third-argument callback and drizzle-kit generate picks the indexes up like any others:

import { integer, pgTable } from 'drizzle-orm/pg-core'
import { encryptedIndexes, types } from '@cipherstash/stack-drizzle'

export const users = pgTable(
  'users',
  {
    id: integer('id').primaryKey(),
    email: types.TextEq('email'),
    bio: types.TextSearch('bio'),
  },
  (t) => [...encryptedIndexes(t)],
)

Each column gets indexes matching its domain's capabilities, named <table>_<column>_<capability> (equality btree, ordering btree, free-text GIN, JSON containment GIN); storage-only and non-encrypted columns get none. After the migration applies, run ANALYZE <table> — expression indexes have no statistics until then. For custom names, subsets, or field-level selector indexes on encrypted JSON, declare individual expression indexes instead; the bundled stash-indexing agent skill has the full recipes.

How it works

Every value is encrypted into an EQL payload: the ciphertext plus the searchable terms its column type declares — an HMAC term for equality, an order-preserving term for range and sorting, a bloom filter for text match, a structured-encryption vector for JSON. The EQL SQL bundle defines the Postgres domains, operators, and term-extractor functions, so WHERE email = $1 resolves to a comparison of equality terms and engages a functional index over the extractor. Keys come from ZeroKMS — one per value — so bulk operations, key revocation, and identity-bound decryption (lock contexts) work without the database ever holding a secret. Runs on plain PostgreSQL, Supabase, and RDS/Aurora; the SQL install needs no superuser.

Docs

Not to be confused with @cipherstash/drizzle, the older @cipherstash/protect-based package — deprecated and no longer maintained; this package replaces it.