Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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-*
Expand Down
6 changes: 3 additions & 3 deletions IA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 …

Expand Down Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion content/docs/get-started/choose-your-stack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions content/docs/integrations/drizzle/api-reference/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "API reference",
"pages": ["index"]
}
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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.
Expand Down Expand Up @@ -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).
Expand All @@ -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` |

Expand All @@ -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

Expand Down Expand Up @@ -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.

<Callout type="warn">
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.
</Callout>

## Insert and read
Expand Down Expand Up @@ -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

<Cards>
<Card title="API reference" href="/integrations/drizzle/api-reference">
Exported column types, schema helpers, codecs, indexes, and encrypted query operators.
</Card>
<Card title="Supabase" href="/integrations/supabase">
Drizzle over a Supabase Postgres connection.
</Card>
Expand Down
5 changes: 5 additions & 0 deletions content/docs/integrations/drizzle/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Drizzle",
"icon": "Drizzle",
"pages": ["index", "api-reference"]
}
2 changes: 1 addition & 1 deletion content/docs/integrations/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Integrations",
"icon": "Blocks",
"pages": ["index", "supabase", "prisma", "..."]
"pages": ["index", "supabase", "prisma", "drizzle", "..."]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions scripts/generate-drizzle-docs.ts
Original file line number Diff line number Diff line change
@@ -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);
});
1 change: 1 addition & 0 deletions scripts/validate-content-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/validate-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
37 changes: 37 additions & 0 deletions src/components/icons/drizzle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function DrizzleIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="13 12.5 64 35"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Drizzle"
{...props}
>
<rect
width="5.41766"
height="22.979"
transform="matrix(0.873028 0.48767 -0.497212 0.867629 24.459 24.3583)"
className="fill-black dark:fill-[#C5F74F]"
/>
<rect
width="5.41766"
height="22.979"
transform="matrix(0.873028 0.48767 -0.497212 0.867629 43.2793 12.6755)"
className="fill-black dark:fill-[#C5F74F]"
/>
<rect
width="5.41766"
height="22.979"
transform="matrix(0.873028 0.48767 -0.497212 0.867629 72.2383 12.676)"
className="fill-black dark:fill-[#C5F74F]"
/>
<rect
width="5.41766"
height="22.979"
transform="matrix(0.873028 0.48767 -0.497212 0.867629 53.4121 24.3583)"
className="fill-black dark:fill-[#C5F74F]"
/>
</svg>
);
}
2 changes: 2 additions & 0 deletions src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, () => React.ReactElement> = {
Drizzle: () => createElement(DrizzleIcon, { width: 16, height: 16 }),
Prisma: () => createElement(PrismaIcon, { width: 16, height: 16 }),
Supabase: () => createElement(SupabaseIcon, { width: 16, height: 16 }),
};
Expand Down