Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fd918a6
docs(v2): Supabase integration section + canonical encryptedSupabase …
coderdan Jul 2, 2026
5fe43c7
docs(v2): restructure Supabase section into a 6-page journey
coderdan Jul 7, 2026
aae5efd
docs(v2): rename Supabase index page title to Overview
coderdan Jul 7, 2026
e5f81e1
docs(supabase): rework "How it works" with corrected EQL type names
coderdan Jul 8, 2026
94053d8
docs(supabase): address PR #39 review feedback
coderdan Jul 8, 2026
d5f46dc
docs(supabase): add reusable FAQ to the overview
coderdan Jul 8, 2026
eeb35df
docs(dynamodb): add reusable FAQ to the DynamoDB page
coderdan Jul 8, 2026
2dbff88
Merge remote-tracking branch 'origin/v2' into cip-3328-supabase
coderdan Jul 9, 2026
5f125b3
docs(supabase): rework onto encryptedSupabaseV3 and the EQL 3.0.0 dom…
coderdan Jul 9, 2026
6a16f79
docs(supabase): state the real cause of the broken substring contains
coderdan Jul 9, 2026
8d4819a
docs(supabase): separate the encryptQuery gap from the PostgREST one
coderdan Jul 9, 2026
748fed7
docs(supabase): point the contains defect at the real cause
coderdan Jul 9, 2026
5c89734
docs(supabase): drop the issue link and tighten the contains callout
coderdan Jul 9, 2026
2f1d093
docs(supabase): drop the contains defect callout, fix nav and overvie…
coderdan Jul 9, 2026
a331d6b
docs(supabase): remove the outdated Dashboard screenshot
coderdan Jul 9, 2026
27d1b6e
docs(supabase): address the preview comments
coderdan Jul 9, 2026
49c60d2
docs(integrations): delete the Next.js page and strip its inbound links
coderdan Jul 9, 2026
b4cea65
docs(integrations): write the Drizzle and Prisma pages, and the secti…
coderdan Jul 9, 2026
ad5b89a
docs(integrations): rename Prisma to Prisma Next
coderdan Jul 9, 2026
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
22 changes: 12 additions & 10 deletions IA.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ so it lives as facets and links, never as a tree section or a hub.
| CTS | Identity service in the Platform | `platform` | `/security/cts`, `/reference/auth/*` |
| `@cipherstash/auth` | Stack package (identity-aware encryption) | `[encryption, platform]` | `/reference/stack/auth` |
| Proxy stack-auth | How auth works inside the Proxy | `[proxy, platform]` | `/reference/proxy/*`, `/security/proxy` |
| Next.js adapter | Framework integration | `[encryption, platform]` | `/integrations/nextjs` |
| Clerk / Auth0 / Okta | Auth-provider integrations | `[platform]` | `/integrations/*` (`category: auth-provider`) |

None of these is filed under an "auth" section, because there isn't one. Each is
Expand Down Expand Up @@ -302,21 +301,20 @@ Two notes:
## Integrations — CIP-3328 (Supabase), CIP-3330 (auth), CIP-3336 (rest)

- [x] Section scaffold 🚧 (index + supabase stub with facet exemplar)
- [ ] `/integrations` index — category grid w/ setup badges
- [ ] `/integrations/supabase` — flagship tutorial (CIP-3328)
- [ ] `/integrations/supabase/database`
- [ ] `/integrations/supabase/auth`
- [ ] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema
- [x] `/integrations` index — category grid w/ setup badges
- [x] `/integrations/supabase` — flagship tutorial (CIP-3328)
- [x] `/integrations/supabase/database`
- [x] `/integrations/supabase/auth`
- [x] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema
- [ ] ⛔ `/integrations/supabase/edge-functions` — pending Deno/FFI answer
- [ ] ⛔ `/integrations/supabase/realtime` — pending product verification
- [ ] `/integrations/drizzle` — merge the two divergent Drizzle pages
- [ ] `/integrations/prisma-next`
- [x] `/integrations/drizzle`
- [x] `/integrations/prisma-next` — EQL v2 today; revisit when v3 support lands
- [ ] `/integrations/aws/rds-aurora` — Proxy path
- [ ] `/integrations/aws/dynamodb`
- [ ] `/integrations/clerk`
- [ ] `/integrations/auth0` — end-to-end example (Clerk parity)
- [ ] `/integrations/okta` — end-to-end example (Clerk parity)
- [ ] `/integrations/nextjs`
- [ ] `/integrations/typescript` — thin router to Stack SDK reference
- [ ] `/integrations/serverless` — Vercel/Lambda, bundling, CS_CONFIG_PATH
- [ ] `/integrations/docker`
Expand Down Expand Up @@ -420,7 +418,7 @@ and the old `/compare/*` paths redirect there (`v2-redirects.mjs`).
- [ ] `/reference/stack` — client + configuration (port encryption/* pages)
- [ ] `/reference/stack/schema`
- [ ] `/reference/stack/encrypt-decrypt` (+ bulk, models)
- [ ] `/reference/stack/supabase` — THE canonical `encryptedSupabase` page, ONE signature (CIP-3328)
- [x] `/reference/stack/supabase` — THE canonical `encryptedSupabase` page, ONE signature (CIP-3328)
- [ ] `/reference/stack/drizzle-operators`
- [ ] `/reference/stack/errors` — port error-handling; miette catalog later (CIP-3338)
- [ ] `/reference/stack/upgrading-from-protect` (retitled package-rename guide)
Expand Down Expand Up @@ -453,5 +451,9 @@ and the old `/compare/*` paths redirect there (`v2-redirects.mjs`).
documents the release as decided, ahead of the eql_v3 branch: payload `v: 3`,
OPE SEM specifier, Docker tag `:17-3.0.0`, `version()` output, schema files.
Each must land upstream or be walked back in the docs before merge
- [ ] ⛔ Stack SDK Supabase-wrapper v3 alignment (CIP-3355, blocks CIP-3335) — the
Supabase section documents the 0.18 wrapper API with v3 wire semantics; the
wrapper itself is still v2 (composite type, `like` wire op, v2 payloads) and
the SDK's v3 branches don't touch `src/supabase/` yet
- [ ] Flip `ENABLE_V2_REDIRECTS=1`, delete `content/stack` + `/stack` routes + legacy loader (CIP-3335)
- [ ] Consistency sweep + Supabase listing v3 revision (CIP-3335)
9 changes: 9 additions & 0 deletions content/docs/concepts/identity-aware-encryption.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Identity-aware encryption
description: "Lock contexts and CTS: binding encrypted values to a user identity so only that identity can decrypt them."
type: concept
---

This page is being built as part of the docs V2 overhaul ([CIP-3330](https://linear.app/cipherstash/issue/CIP-3330)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).

Until it lands, the current version lives in the [existing docs](/stack/cipherstash/encryption/identity).
9 changes: 9 additions & 0 deletions content/docs/guides/development/schema-design.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Schema design
description: "Choosing the right encrypted type and capability for each column."
type: guide
---

This page is being built as part of the docs V2 overhaul ([CIP-3327](https://linear.app/cipherstash/issue/CIP-3327)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).

Until it lands, [EQL core concepts](/reference/eql/core-concepts) covers the capability model, and the per-type pages ([numbers](/reference/eql/numbers), [dates & times](/reference/eql/dates-and-times), [text](/reference/eql/text), [JSON](/reference/eql/json)) cover choosing variants.
9 changes: 9 additions & 0 deletions content/docs/guides/migration/encrypt-existing-data.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Encrypt existing data
description: "Backfilling encryption onto live tables, column by column."
type: guide
---

This page is being built as part of the docs V2 overhaul ([CIP-3329](https://linear.app/cipherstash/issue/CIP-3329)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).

Until it lands, the current version lives in the [existing docs](/stack/cipherstash/proxy/encrypt-tool).
179 changes: 179 additions & 0 deletions content/docs/integrations/drizzle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
title: Drizzle
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]
audience: [developer]
integration:
category: orm
setup: code-only
pairsWith: [supabase, prisma-next]
verifiedAgainst:
stack: "1.0.0"
eql: "3.0.0"
---

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.

## Install

```bash cta cta-type="install" example-id="install-drizzle"
npm install @cipherstash/stack drizzle-orm
```

Then install EQL into your database:

```bash
npx stash eql install --eql-version 3
```

<Callout type="warn">
`--eql-version 3` installs directly against the database and cannot generate a Drizzle migration: `--drizzle` is rejected alongside it. EQL is therefore not part of your migration history, so a database rebuilt from migrations will not have it. Re-run the install after a rebuild.
</Callout>

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

```typescript filename="src/schema.ts"
import { pgTable, integer } from "drizzle-orm/pg-core"
import { types } from "@cipherstash/stack/eql/v3/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 containment
balance: types.Bigint("balance"), // storage only
})
```

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

| Factory | Capability | EQL domain |
| --- | --- | --- |
| `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.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` |

The same bare / `Eq` / `Ord` pattern exists for `Smallint`, `Bigint`, `Real`, `Double`, `Numeric`, `Date`, and `Timestamp`. `Boolean` is storage-only by design, because a two-value column has too little cardinality for any searchable index to be safe.

Declare only the capability you query on. Each one stores an extra index term alongside the ciphertext, with a bounded, published leakage profile: see [searchable encryption](/concepts/searchable-encryption).

## Wire up the client

Derive the encryption schema from the Drizzle table, build a typed client, and create the operators:

```typescript filename="src/db.ts"
import { drizzle } from "drizzle-orm/postgres-js"
import {
createEncryptionOperatorsV3,
extractEncryptionSchemaV3,
} from "@cipherstash/stack/eql/v3/drizzle"
import { EncryptionV3 } from "@cipherstash/stack/v3"
import { users } from "./schema"

export const usersSchema = extractEncryptionSchemaV3(users)

export const client = await EncryptionV3({ schemas: [usersSchema] })
export const ops = createEncryptionOperatorsV3(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.

## Query

Every where-clause operator is **async**, because it encrypts its operand before Drizzle sees it. `await` each one. `ops.asc` and `ops.desc` are synchronous.

```typescript filename="src/queries.ts"
import { db, ops } from "./db"
import { users } from "./schema"

// Equality — email is TextEq
const exact = await db.select().from(users)
.where(await ops.eq(users.email, "alice@example.com"))

// Ranges and ordering — age is IntegerOrd
const adults = await db.select().from(users)
.where(await ops.gte(users.age, 18))
.orderBy(ops.asc(users.age))

const midBand = await db.select().from(users)
.where(await ops.between(users.age, 25, 40))

// Set membership, built on equality
const listed = await db.select().from(users)
.where(await ops.inArray(users.email, ["alice@example.com", "bob@example.com"]))

// Free-text token containment — bio is TextMatch
const coffee = await db.select().from(users)
.where(await ops.contains(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.

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.
</Callout>

## Insert and read

Rows are encrypted **before** they reach Drizzle, so Drizzle never sees plaintext. Encrypt a batch in one call, which is also one ZeroKMS round trip:

```typescript filename="src/insert.ts"
import { client, db, usersSchema } from "./db"
import { users } from "./schema"

const rows = await client.bulkEncryptModels(
[
{ email: "alice@example.com", age: 30, bio: "climbing and coffee", balance: 100_000n },
{ email: "bob@example.com", age: 41, bio: "cycling and coffee", balance: 250_000n },
],
usersSchema,
)

if (rows.failure) {
throw new Error(rows.failure.message)
}

await db.insert(users).values(rows.data)
```

`Bigint` columns take a native JS `bigint`. Decrypt what comes back with `client.bulkDecryptModels(rows)`, which is likewise one round trip for the batch.

## 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;
```

Index selection, `EXPLAIN` verification, and large-table build guidance are in [EQL indexes](/reference/eql/indexes).

## Where to next

<Cards>
<Card title="Supabase" href="/integrations/supabase">
Drizzle over a Supabase Postgres connection.
</Card>
<Card title="EQL core concepts" href="/reference/eql/core-concepts">
The variant model behind each column type.
</Card>
<Card title="Searchable encryption" href="/concepts/searchable-encryption">
What each capability reveals, and how to choose.
</Card>
<Card title="EQL indexes" href="/reference/eql/indexes">
The functional-index recipes behind every query on this page.
</Card>
</Cards>
64 changes: 60 additions & 4 deletions content/docs/integrations/index.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,65 @@
---
title: Integrations
description: "Set up CipherStash with your platform, ORM, framework, auth provider, and runtime."
type: tutorial
description: "Set up CipherStash with your Postgres platform, your ORM, and your identity provider."
type: concept
components: [encryption, eql, platform]
audience: [developer]
---

This section is being built as part of the docs V2 overhaul ([CIP-3307](https://linear.app/cipherstash/issue/CIP-3307)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).
CipherStash encrypts fields in your application and keeps them queryable in Postgres. Which integration you reach for depends on how your application talks to the database, not on what you are encrypting.

Until it lands, current documentation lives in the [existing docs](/stack).
Everything here writes the same ciphertext into the same [EQL](/reference/eql) columns, so these are not one-way doors. You can move a table from one path to another without re-encrypting it, and mix them in one codebase: an ORM for application queries, raw SQL for migrations.

## Where your data lives

<Cards>
<Card title="Supabase" href="/integrations/supabase">
Wrap the Supabase client so `.eq()`, `.gt()`, and `.order()` keep working on encrypted columns. Row Level Security and Supabase Auth apply unchanged.
</Card>
<Card title="Any other Postgres" href="/get-started/quickstart">
Neon, RDS, Aurora, Cloud SQL, or a container. EQL installs into any Postgres you can connect to, with no extension and no superuser.
</Card>
</Cards>

## How you query it

<Cards>
<Card title="Drizzle" href="/integrations/drizzle">
Concrete encrypted column types and capability-checked operators. Targets EQL v3.
</Card>
<Card title="Prisma Next" href="/integrations/prisma-next">
Encrypted columns declared in `schema.prisma`. Targets EQL v2. Not classic Prisma.
</Card>
<Card title="Raw SQL" href="/get-started/quickstart">
Encrypt the value, insert it, cast the query operand. The SDK's whole surface is `encrypt` and `encryptQuery`.
</Card>
<Card title="An ORM we don't list" href="/reference/stack">
Encrypt before a write, decrypt after a read. Every ORM supports that, and nothing is missing.
</Card>
</Cards>

<Callout type="info">
Not using Postgres? The SDK still encrypts values that never touch a database, and non-Postgres stores like [DynamoDB](/stack/cipherstash/encryption/dynamodb), with no EQL involved. EQL is the Postgres surface, not the encryption itself.
</Callout>

## Who is allowed to decrypt

Reach for identity binding when a value should be decryptable only by the user it belongs to, rather than by anything holding the application's credentials. It needs an OIDC provider, registered once in the [Dashboard](https://dashboard.cipherstash.com/workspaces/_/oidc-providers).

| Provider | Supported |
| --- | --- |
| Supabase Auth | Yes. See [Supabase Auth](/integrations/supabase/auth) |
| Clerk | Yes |
| Auth0 | Yes |
| Okta | Yes |
| Any OIDC issuer | Yes, if it publishes a discovery endpoint |

See [provable access control](/solutions/provable-access) for what this buys you, and what it does not.

## You cannot change the application

[CipherStash Proxy](/reference/proxy) sits in front of Postgres and speaks EQL for you. Point your connection string at it. Because it speaks the Postgres wire protocol, the language your application is written in stops mattering.

## Still deciding

[Choose your stack](/get-started/choose-your-stack) walks the four decisions in order: SDK or Proxy, your Postgres, your ORM, and your identity provider.
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": ["..."]
"pages": ["index", "supabase", "..."]
}
Loading