diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 08b68f9..bdf3caf 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -52,6 +52,9 @@ jobs:
- name: Generate the Supabase reference
run: bun run generate-docs:supabase
+ - name: Generate the Drizzle reference
+ run: bun run generate-docs:drizzle
+
- name: Generate the Prisma reference
run: bun run generate-docs:prisma
diff --git a/.gitignore b/.gitignore
index 641a8b8..ac2a70f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,8 @@ content/docs/integrations/prisma/api-reference/*
!content/docs/integrations/prisma/api-reference/meta.json
content/docs/integrations/supabase/api-reference/*
!content/docs/integrations/supabase/api-reference/meta.json
+content/docs/integrations/drizzle/api-reference/*
+!content/docs/integrations/drizzle/api-reference/meta.json
content/docs/reference/stack/api-reference/*
!content/docs/reference/stack/api-reference/meta.json
.tmp-*
diff --git a/IA.md b/IA.md
index 38a9f69..c3ef001 100644
--- a/IA.md
+++ b/IA.md
@@ -159,7 +159,7 @@ Solutions
└─ Protecting PII · Healthcare/HIPAA · AI & RAG · Data residency · Provable access
Reference
├─ EQL core-concepts · numbers · dates · text · json · … · joins
-├─ Stack SDK client · schema · encrypt-decrypt · supabase · drizzle-operators · errors
+├─ Stack SDK overview · usage · generated package API
├─ Auth lock-contexts · cts-tokens · oidc · access-keys
├─ CLI · Proxy · Workspace
└─ Benchmarks · Agent skills · Glossary
@@ -200,7 +200,7 @@ layer, not a layer every Stack page sits on.
- Start here: Quickstart · Reference → Stack SDK (client + configuration)
- Concepts: application-level encryption · searchable encryption · identity-aware encryption
- Guides: schema design · encrypt existing data · testing & CI · serverless & bundling
-- Reference: `/reference/stack/*` (schema · encrypt-decrypt · supabase · drizzle-operators · errors)
+- Reference: `/reference/stack/*` (overview · usage · generated package API)
- Security: `/security/stack-sdk`
- Integrations (auto): Supabase · Drizzle · Prisma · DynamoDB · Next.js …
@@ -308,7 +308,7 @@ Two notes:
- [x] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema
- [ ] ⛔ `/integrations/supabase/edge-functions` — pending Deno/FFI answer
- [ ] ⛔ `/integrations/supabase/realtime` — pending product verification
-- [x] `/integrations/drizzle`
+- [x] `/integrations/drizzle` — overview and generated `@cipherstash/stack-drizzle` API reference
- [x] `/integrations/prisma` — Prisma ORM 8 RC, EQL v3, Prisma Postgres, Prisma Compute, and generated API reference
- [ ] `/integrations/aws/rds-aurora` — Proxy path
- [ ] `/integrations/aws/dynamodb`
diff --git a/content/docs/get-started/choose-your-stack.mdx b/content/docs/get-started/choose-your-stack.mdx
index e2c4963..b6cc077 100644
--- a/content/docs/get-started/choose-your-stack.mdx
+++ b/content/docs/get-started/choose-your-stack.mdx
@@ -44,7 +44,7 @@ Managed platforms usually block custom operator class creation. That's why the d
| Query layer | Path | What it looks like |
|---|---|---|
-| Drizzle | [Drizzle integration](/stack/cipherstash/encryption/drizzle) | An `encryptedType` column and typed query operators. Your Drizzle code keeps its shape. |
+| Drizzle | [Drizzle integration](/integrations/drizzle) | Encrypted column types and typed query operators. Your Drizzle code keeps its shape. |
| Supabase JS | [Supabase integration](/integrations/supabase) | `encryptedSupabase` wraps the client. `.eq()`, `.gt()`, `.order()` keep working. |
| Prisma ORM 8 RC | [Prisma integration](/integrations/prisma) | Encrypted fields and query operators are generated from `schema.prisma`. |
| Raw SQL | [Quickstart](/get-started/quickstart) | Encrypt the value, insert it, cast the query operand. |
diff --git a/content/docs/get-started/quickstart.mdx b/content/docs/get-started/quickstart.mdx
index 086fec5..3368e3d 100644
--- a/content/docs/get-started/quickstart.mdx
+++ b/content/docs/get-started/quickstart.mdx
@@ -179,7 +179,7 @@ You encrypted a field, stored it, queried it without decrypting, and read it bac
## Next steps
-- **Using an ORM or Supabase?** The [Drizzle](/stack/cipherstash/encryption/drizzle) and [Supabase](/integrations/supabase) integrations wrap your existing client so queries look normal.
+- **Using an ORM or Supabase?** The [Drizzle](/integrations/drizzle) and [Supabase](/integrations/supabase) integrations wrap your existing client so queries look normal.
- **No app changes possible?** [CipherStash Proxy](/reference/proxy) sits in front of Postgres and does this transparently.
- **Bind decryption to a user.** [Provable access control](/solutions/provable-access) ties a value to the identity that encrypted it.
- **Ready to deploy?** [Deployment](/guides/deployment) covers credential choices, environment promotion, and production rollout gates.
diff --git a/content/docs/integrations/drizzle/api-reference/meta.json b/content/docs/integrations/drizzle/api-reference/meta.json
new file mode 100644
index 0000000..dd99951
--- /dev/null
+++ b/content/docs/integrations/drizzle/api-reference/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "API reference",
+ "pages": ["index"]
+}
diff --git a/content/docs/integrations/drizzle.mdx b/content/docs/integrations/drizzle/index.mdx
similarity index 69%
rename from content/docs/integrations/drizzle.mdx
rename to content/docs/integrations/drizzle/index.mdx
index e099aca..7900097 100644
--- a/content/docs/integrations/drizzle.mdx
+++ b/content/docs/integrations/drizzle/index.mdx
@@ -1,5 +1,5 @@
---
-title: Drizzle
+title: Overview
description: "Encrypted columns with Drizzle ORM: pick a concrete EQL column type, and capability-checked operators encrypt your query operands for you."
type: tutorial
components: [encryption, eql]
@@ -9,8 +9,8 @@ integration:
setup: code-only
pairsWith: [supabase, prisma]
verifiedAgainst:
- stack: "1.0.0-rc.0"
- eql: "3.0.0"
+ stack: "1.0.0-rc.4"
+ eql: "3.0.4"
---
Drizzle keeps its shape. You declare each encrypted column with a concrete EQL type, and a set of query operators encrypt their operands before Drizzle builds the SQL. Your `select`, `where`, and `orderBy` read as they always did, and the database only ever compares ciphertext.
@@ -39,19 +39,23 @@ The native Stack client used below discovers the four `CS_*` variables first, th
## Declare the table
-The column type *is* the capability. There are no flags to configure. `TextEq` answers equality and nothing else, `IntegerOrd` answers ranges and ordering, `TextMatch` answers free-text containment, and a bare `Text` or `Bigint` is storage-only.
+The column type *is* the capability. There are no flags to configure. `TextEq` answers equality and nothing else, `IntegerOrd` answers ranges and ordering, `TextMatch` answers free-text matching, and a bare `Text` or `Bigint` is storage-only.
```typescript filename="src/schema.ts"
import { pgTable, integer } from "drizzle-orm/pg-core"
-import { types } from "@cipherstash/stack-drizzle/v3"
-
-export const users = pgTable("users", {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
- email: types.TextEq("email"), // equality
- age: types.IntegerOrd("age"), // ranges, ordering, and equality
- bio: types.TextMatch("bio"), // free-text containment
- balance: types.Bigint("balance"), // storage only
-})
+import { encryptedIndexes, types } from "@cipherstash/stack-drizzle"
+
+export const users = pgTable(
+ "users",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ email: types.TextEq("email"), // equality
+ age: types.IntegerOrd("age"), // ranges, ordering, and equality
+ bio: types.TextMatch("bio"), // free-text token matching
+ balance: types.Bigint("balance"), // storage only
+ },
+ (table) => [...encryptedIndexes(table)],
+)
```
Each factory maps to the matching EQL domain, so `types.TextEq("email")` types the column as `public.eql_v3_text_eq`. The variant model behind those names is in [core concepts](/reference/eql/core-concepts).
@@ -61,7 +65,7 @@ Each factory maps to the matching EQL domain, so `types.TextEq("email")` types t
| `types.Text(name)` | Store and decrypt only | `public.eql_v3_text` |
| `types.TextEq(name)` | `eq`, `ne`, `inArray`, `notInArray` | `public.eql_v3_text_eq` |
| `types.TextOrd(name)` | Ranges, ordering, plus equality | `public.eql_v3_text_ord` |
-| `types.TextMatch(name)` | `contains` | `public.eql_v3_text_match` |
+| `types.TextMatch(name)` | `matches` | `public.eql_v3_text_match` |
| `types.TextSearch(name)` | All of the above, on text | `public.eql_v3_text_search` |
| `types.IntegerOrd(name)` | Ranges, ordering, plus equality | `public.eql_v3_integer_ord` |
@@ -76,21 +80,21 @@ Derive the encryption schema from the Drizzle table, build a typed client, and c
```typescript filename="src/db.ts"
import { drizzle } from "drizzle-orm/postgres-js"
import {
- createEncryptionOperatorsV3,
- extractEncryptionSchemaV3,
-} from "@cipherstash/stack-drizzle/v3"
-import { EncryptionV3 } from "@cipherstash/stack/v3"
+ createEncryptionOperators,
+ extractEncryptionSchema,
+} from "@cipherstash/stack-drizzle"
+import { Encryption } from "@cipherstash/stack"
import { users } from "./schema"
-export const usersSchema = extractEncryptionSchemaV3(users)
+export const usersSchema = extractEncryptionSchema(users)
-export const client = await EncryptionV3({ schemas: [usersSchema] })
-export const ops = createEncryptionOperatorsV3(client)
+export const client = await Encryption({ schemas: [usersSchema] })
+export const ops = createEncryptionOperators(client)
export const db = drizzle({ client: sqlClient })
```
-`extractEncryptionSchemaV3` reads the encryption config straight off the column types, so there is no second schema to keep in sync with the first.
+`extractEncryptionSchema` reads the encryption config straight off the column types, so there is no second schema to keep in sync with the first.
## Query
@@ -118,15 +122,15 @@ const listed = await db.select().from(users)
// Free-text token containment — bio is TextMatch
const coffee = await db.select().from(users)
- .where(await ops.contains(users.bio, "coffee"))
+ .where(await ops.matches(users.bio, "coffee"))
```
-The full operator set is `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `notBetween`, `inArray`, `notInArray`, `contains`, `exists`, `notExists`, `and`, `or`, `not`, `isNull`, `isNotNull`, `asc`, and `desc`. Null checks need no encryption, because a SQL `NULL` is never encrypted.
+The full operator set is `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `notBetween`, `matches`, `contains`, `selector`, `inArray`, `notInArray`, `exists`, `notExists`, `and`, `or`, `not`, `isNull`, `isNotNull`, `asc`, and `desc`. Null checks need no encryption, because a SQL `NULL` is never encrypted. See the [API reference](/integrations/drizzle/api-reference#createencryptionoperators) for signatures and per-operator capability requirements.
Applying an operator the column's type cannot answer throws `EncryptionOperatorError` rather than silently returning wrong rows. Ordering a `TextEq` column, for instance, has no ordering term to compare.
-There is no `like` or `ilike`. SQL pattern matching is meaningless on ciphertext, so free-text search is `ops.contains`, which compares encrypted token sets. It matches whole indexed tokens rather than arbitrary substrings.
+There is no `like` or `ilike`. SQL pattern matching is meaningless on ciphertext, so free-text search is `ops.matches`, which compares encrypted token sets. It matches whole indexed tokens rather than arbitrary substrings. `ops.contains` is reserved for encrypted JSON containment.
## Insert and read
@@ -156,20 +160,16 @@ await db.insert(users).values(rows.data)
## Indexes
-At real row counts, add a functional index for each capability you query. The encrypted operators inline into these extractor functions, so an ordinary `CREATE INDEX` is all it takes:
-
-```sql filename="indexes.sql"
-CREATE INDEX users_email_eq ON users USING hash (eql_v3.eq_term(email));
-CREATE INDEX users_age_ord ON users USING btree (eql_v3.ord_term(age));
-CREATE INDEX users_bio_match ON users USING gin (eql_v3.match_term(bio));
-ANALYZE users;
-```
+`encryptedIndexes(table)` derives the recommended functional indexes from each encrypted column's capabilities. In the schema above it produces equality, ordering, and free-text indexes while ignoring the storage-only `balance` column. Generate and apply the result through your normal `drizzle-kit` migration workflow, then run `ANALYZE users`.
-Index selection, `EXPLAIN` verification, and large-table build guidance are in [EQL indexes](/reference/eql/indexes).
+See [`encryptedIndexes`](/integrations/drizzle/api-reference#encryptedindexes) for the emitted index forms. Index selection, `EXPLAIN` verification, and large-table build guidance are in [EQL indexes](/reference/eql/indexes).
## Where to next
+
+ Exported column types, schema helpers, codecs, indexes, and encrypted query operators.
+
Drizzle over a Supabase Postgres connection.
diff --git a/content/docs/integrations/drizzle/meta.json b/content/docs/integrations/drizzle/meta.json
new file mode 100644
index 0000000..d185819
--- /dev/null
+++ b/content/docs/integrations/drizzle/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "Drizzle",
+ "icon": "Drizzle",
+ "pages": ["index", "api-reference"]
+}
diff --git a/content/docs/integrations/meta.json b/content/docs/integrations/meta.json
index 15136e6..78d30fa 100644
--- a/content/docs/integrations/meta.json
+++ b/content/docs/integrations/meta.json
@@ -1,5 +1,5 @@
{
"title": "Integrations",
"icon": "Blocks",
- "pages": ["index", "supabase", "prisma", "..."]
+ "pages": ["index", "supabase", "prisma", "drizzle", "..."]
}
diff --git a/package.json b/package.json
index 2b9a412..f851fd1 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "prebuild": "bun run generate-docs && bun run generate-docs:supabase && bun run generate-docs:prisma && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects",
+ "prebuild": "bun run generate-docs && bun run generate-docs:supabase && bun run generate-docs:drizzle && bun run generate-docs:prisma && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects",
"build": "next build",
"dev": "next dev -p 3001",
"start": "next start",
@@ -13,6 +13,7 @@
"format": "biome format --write",
"generate-docs": "tsx scripts/generate-docs.ts",
"generate-docs:supabase": "tsx scripts/generate-supabase-docs.ts",
+ "generate-docs:drizzle": "tsx scripts/generate-drizzle-docs.ts",
"generate-docs:prisma": "tsx scripts/generate-prisma-docs.ts",
"generate-docs:eql": "tsx scripts/generate-eql-docs.ts",
"generate-docs:eql-api": "tsx scripts/generate-eql-api-docs.ts",
diff --git a/scripts/generate-drizzle-docs.ts b/scripts/generate-drizzle-docs.ts
new file mode 100644
index 0000000..a66af22
--- /dev/null
+++ b/scripts/generate-drizzle-docs.ts
@@ -0,0 +1,37 @@
+#!/usr/bin/env tsx
+/** Generate the Drizzle ORM integration's API reference from Stack main. */
+import path from "node:path";
+import { type DocsConfig, generateDocs } from "./lib/docs-generator.js";
+
+const drizzleConfig: DocsConfig = {
+ packageName: "@cipherstash/stack-drizzle",
+ projectName: "@cipherstash/stack-drizzle",
+ repoUrl: "https://github.com/cipherstash/stack.git",
+ sourceRef: "main",
+ tempDirName: ".tmp-stack-drizzle",
+ baseOutputDir: path.join(
+ process.cwd(),
+ "content/docs/integrations/drizzle/api-reference",
+ ),
+ publicPath: "/integrations/drizzle/api-reference",
+ metaTitle: "API reference",
+ versionedOutput: false,
+ entryPointBasePath: "packages/stack-drizzle/src",
+ entryModule: "index",
+ router: "module",
+ flattenOutputFiles: true,
+ entryPoints: ["./packages/stack-drizzle/src/index.ts"],
+ tsconfigInclude: ["packages/stack-drizzle/src/**/*"],
+ tagFilter: () => false,
+ referencePathSegment: "drizzle",
+ frontmatterGlobals: {
+ type: "reference",
+ components: ["encryption", "eql"],
+ audience: ["developer"],
+ },
+};
+
+generateDocs(drizzleConfig).catch((error) => {
+ console.error("Failed to generate the Drizzle API reference:", error);
+ process.exit(1);
+});
diff --git a/scripts/validate-content-api.ts b/scripts/validate-content-api.ts
index 56fff7c..4490f1b 100644
--- a/scripts/validate-content-api.ts
+++ b/scripts/validate-content-api.ts
@@ -37,6 +37,7 @@ const SKIP_PATHS = [
"content/stack/reference/stack/", // TypeDoc output
"content/docs/reference/stack/api-reference/", // TypeDoc output
"content/docs/integrations/supabase/api-reference/", // TypeDoc output
+ "content/docs/integrations/drizzle/api-reference/", // TypeDoc output
"content/docs/integrations/prisma/api-reference/", // TypeDoc output
"content/stack/reference/eql/index.mdx", // generated EQL API reference
"content/docs/reference/eql/functions.mdx", // generated
diff --git a/scripts/validate-links.ts b/scripts/validate-links.ts
index 1ba7014..b4c34e9 100644
--- a/scripts/validate-links.ts
+++ b/scripts/validate-links.ts
@@ -361,6 +361,7 @@ allBroken.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
const GENERATED = [
"content/docs/reference/stack/api-reference/index.mdx",
"content/docs/integrations/supabase/api-reference/index.mdx",
+ "content/docs/integrations/drizzle/api-reference/index.mdx",
"content/docs/integrations/prisma/api-reference/index.mdx",
];
const missing = GENERATED.filter((d) => !fs.existsSync(path.join(ROOT, d)));
diff --git a/src/components/icons/drizzle.tsx b/src/components/icons/drizzle.tsx
new file mode 100644
index 0000000..9c4ee5e
--- /dev/null
+++ b/src/components/icons/drizzle.tsx
@@ -0,0 +1,37 @@
+export function DrizzleIcon(props: React.SVGProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/source.ts b/src/lib/source.ts
index 6c2080d..7c990b3 100644
--- a/src/lib/source.ts
+++ b/src/lib/source.ts
@@ -3,10 +3,12 @@ import type * as PageTree from "fumadocs-core/page-tree";
import { type InferPageType, loader } from "fumadocs-core/source";
import { icons } from "lucide-react";
import { createElement } from "react";
+import { DrizzleIcon } from "@/components/icons/drizzle";
import { PrismaIcon } from "@/components/icons/prisma";
import { SupabaseIcon } from "@/components/icons/supabase";
const customIcons: Record React.ReactElement> = {
+ Drizzle: () => createElement(DrizzleIcon, { width: 16, height: 16 }),
Prisma: () => createElement(PrismaIcon, { width: 16, height: 16 }),
Supabase: () => createElement(SupabaseIcon, { width: 16, height: 16 }),
};