Skip to content
Open
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
16 changes: 10 additions & 6 deletions IA.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,13 @@ Two notes:

- [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/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/drizzle` 🚧 — merge the two divergent Drizzle pages
- [ ] `/integrations/prisma-next`
- [ ] `/integrations/aws/rds-aurora` — Proxy path
- [ ] `/integrations/aws/dynamodb`
Expand Down Expand Up @@ -420,7 +420,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 +453,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).
13 changes: 13 additions & 0 deletions content/docs/integrations/drizzle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Drizzle
description: "Encrypted columns with Drizzle ORM."
type: tutorial
integration:
category: orm
setup: code-only
pairsWith: [supabase, nextjs]
---

This page is being built as part of the docs V2 overhaul ([CIP-3336](https://linear.app/cipherstash/issue/CIP-3336)). 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/drizzle).
13 changes: 13 additions & 0 deletions content/docs/integrations/nextjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Next.js
description: "Application-level encryption in a Next.js app: where encryption runs across Server Components, Route Handlers, and Server Actions."
type: tutorial
integration:
category: framework
setup: code-only
pairsWith: [supabase, drizzle, prisma]
---

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

Until it lands, current documentation lives in the [existing docs](/stack).
13 changes: 13 additions & 0 deletions content/docs/integrations/prisma.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Prisma
description: "Encrypted columns with Prisma ORM."
type: tutorial
integration:
category: orm
setup: code-only
pairsWith: [supabase, nextjs]
---

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

Until it lands, the current version lives in the [existing docs](/stack/cipherstash/encryption/prisma-next).
131 changes: 131 additions & 0 deletions content/docs/integrations/supabase/auth.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
title: Supabase Auth
description: "Federate the Supabase Auth session into CipherStash so encryption authenticates as the signed-in user — then, optionally, lock decryption to that user's identity."
type: guide
components: [encryption, platform]
audience: [developer]
verifiedAgainst:
stack: "0.18.0"
auth: "0.41.0"
---

Supabase Auth already knows who your user is. This page connects that identity to CipherStash in two layers you can adopt independently:

1. **Federation** — exchange the user's Supabase session for a CipherStash token, so every encryption and decryption request authenticates *as that user* instead of as a shared service credential. This is the foundation.
2. **Identity-bound encryption (lock context)** — an optional layer on top of federation that binds a value to the user's identity claim, so only that user can decrypt it — enforced by ZeroKMS, not by your application code.

Federation is useful on its own. Lock context requires it. Start with federation; add lock context where per-user secrecy matters.

## Register Supabase as an OIDC provider

CipherStash needs to trust your Supabase project as an OIDC issuer before it will accept a Supabase session token. Add the provider once, in the CipherStash dashboard at [dashboard.cipherstash.com/workspaces/_/oidc-providers](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) (the `_` resolves to whichever workspace you select). A Supabase project's issuer is:

```
https://<project-ref>.supabase.co/auth/v1
```

> **Good to know**: the dashboard's [Supabase integration](/integrations/supabase/fundamentals#connecting-your-project-to-cipherstash) can register this for you in one click while it's connected to your project over OAuth. Manual OIDC configuration is covered in the [auth reference](/reference/auth).

## Federate the Supabase session

Authenticate the `Encryption` client with an `OidcFederationStrategy`. It takes your workspace CRN and a `getJwt` callback that returns the user's **current** Supabase access token — the strategy calls it on first use and again on every re-federation, so return a fresh token each time rather than capturing one:

```typescript title="lib/db.ts"
import { createClient } from "@supabase/supabase-js"
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"
import { encryptedSupabase } from "@cipherstash/stack/supabase"
import { patients } from "./schema"

const supabaseClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
)

// Return the current Supabase session token — re-invoked on every re-federation.
const getJwt = async () => {
const { data: { session } } = await supabaseClient.auth.getSession()
if (!session) throw new Error("No active Supabase session")
return session.access_token
}

const encryptionClient = await Encryption({
schemas: [patients],
config: {
authStrategy: OidcFederationStrategy.create(process.env.CS_WORKSPACE_CRN!, getJwt),
},
})

export const db = encryptedSupabase({ encryptionClient, supabaseClient })
```

That's the whole integration. Every `db.from(...)` query now encrypts and decrypts under the signed-in user's identity, and your Supabase Auth session and RLS policies apply to the underlying request exactly as before.

> **Good to know**: the federation endpoint issues no refresh token — when the CipherStash token expires, the strategy re-federates by calling `getJwt` again. Because `getJwt` reads from `supabaseClient.auth.getSession()`, supabase-js's own token refresh keeps it valid. This is also why the strategy runs **server-side only** (a Next.js Route Handler / Server Action, an Edge Function) — never in the browser, where it would expose the session token. For the Edge runtime, see [Edge Functions](/integrations/supabase/edge-functions).

## Identity-bound encryption (lock context)

Federation authenticates *as* the user. **Lock context** goes further: it bakes a claim from the user's JWT (by default `sub`, which for Supabase Auth is the user's UUID) into the data key, so a value can only be decrypted by presenting the same identity. Even your own backend — holding valid workspace credentials — cannot decrypt another user's locked values.

<Callout type="warn">
Lock context requires the client to be authenticated with `OidcFederationStrategy` (above). It cannot be used with `AccessKeyStrategy`, which authenticates a *service*, not a user — there is no user `sub` claim to bind to. Lock context also requires a Business or Enterprise workspace plan.
</Callout>

Attach a lock context to any [`encryptedSupabase`](/reference/stack/supabase) query with `.withLockContext()`, passing the identity claim to bind:

```typescript
import { db } from "./lib/db"
import { patients } from "./lib/schema"

// Insert — the value is bound to the signed-in user's `sub` claim
await db.from("patients", patients)
.insert({ email: "alice@example.com", name: "Alice Chen" })
.withLockContext({ identityClaim: ["sub"] })

// Read — the same claim must be supplied, and the same user must be authenticated
const { data } = await db.from("patients", patients)
.select("id, email, name")
.eq("email", "alice@example.com")
.withLockContext({ identityClaim: ["sub"] })
```

ZeroKMS resolves the claim's *value* from the token that authenticated the request — the federated Supabase identity — so no separate identify step or per-operation token is needed. Reading a locked value without a matching context, or as a different user, fails with an encryption error regardless of what RLS allows.

<Callout type="info">
Earlier releases used `new LockContext().identify(jwt)` to fetch a per-operation token. That flow is **deprecated** — per-operation CTS tokens were removed in protect-ffi 0.25. Authenticate the client with `OidcFederationStrategy` and pass the claim directly to `.withLockContext()`, as above.
</Callout>

## Where it fits

- **Per-user data** (a user's own medical history, messages, documents): lock to `sub`. The row is useless to anyone but its owner — including operators with database access and your own service role.
- **Shared / tenant data** (a support queue, org-wide records): don't lock it, or you'll be unable to decrypt it outside a user session. Federation alone still authenticates access as the user; RLS scopes which rows they see.
- **Server-side jobs** that must read locked data need the owning user's session — by design. If a background job must read a field, that field shouldn't be identity-locked.

## Errors

Both federation and lock context surface failures as values, not throws.

| Scenario | What you see |
| --- | --- |
| Provider not registered with the workspace | Federation fails — register the Supabase OIDC issuer first |
| Expired or invalid Supabase session | `getJwt` throws / returns no session; federation cannot proceed |
| CipherStash token expired mid-request | Strategy re-federates automatically via `getJwt` |
| Decrypting another user's locked value | `encryptionError` on the [query response](/reference/stack/supabase#responses-and-errors) |

The strategy's own factory and token errors follow the `@cipherstash/auth` `Result` shape (`failure.type`); see the [auth reference](/reference/auth).

## Where to next

<Cards>
<Card title="Identity-aware encryption" href="/concepts/identity-aware-encryption">
The concept: federation, lock contexts, and what identity binding proves.
</Card>
<Card title="Edge Functions" href="/integrations/supabase/edge-functions">
Run the federation strategy server-side in Supabase Edge Functions.
</Card>
<Card title="encryptedSupabase reference" href="/reference/stack/supabase">
`.withLockContext()` and `.audit()` on the query builder.
</Card>
<Card title="Auth reference" href="/reference/auth">
OIDC providers, federation strategies, and access keys.
</Card>
</Cards>
127 changes: 127 additions & 0 deletions content/docs/integrations/supabase/edge-functions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: Edge Functions
description: "Run CipherStash encryption inside Supabase Edge Functions using the @cipherstash/stack WebAssembly build, with the CipherStash token cached across invocations in an HTTP-only cookie."
type: guide
components: [encryption, platform]
audience: [developer]
verifiedAgainst:
stack: "0.18.0"
auth: "0.41.0"
---

Encryption and decryption must run somewhere your keys and credentials are safe — never in the browser. Supabase Edge Functions are that place: server-side compute next to your database. CipherStash ships a **WebAssembly build** of the SDK for exactly this runtime, so the same encrypt/decrypt code runs in Deno with no native bindings.

<Callout type="info">
**Verifying the Supabase wrapper on the edge.** The `@cipherstash/stack/wasm-inline` entry exports the core `Encryption` client (`client.encrypt` / `client.decrypt`), which this page uses. Whether the higher-level [`encryptedSupabase`](/reference/stack/supabase) wrapper is available on the edge build is being confirmed — this page will be updated with the wrapper pattern once verified. Until then, use the core client shown below inside your Edge Function.
</Callout>

## Why the wasm build

Supabase Edge Functions run on Deno, which loads npm packages through the `node` compatibility layer. The SDK's default entry resolves to native NAPI bindings that don't exist in that runtime, so the edge uses a dedicated entry: `@cipherstash/stack/wasm-inline`. It embeds the WebAssembly module as base64 inside the JS bundle — no separate `.wasm` fetch, no bundler plugins, no `static_files` config. The same applies to the auth package (`@cipherstash/auth/wasm-inline`).

## Set up the function

Map the `wasm-inline` entries in your function's `deno.json`:

```jsonc title="supabase/functions/encrypt/deno.json"
{
"imports": {
"@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@^0.18/wasm-inline",
"@cipherstash/auth/wasm-inline": "npm:@cipherstash/auth@^0.41/wasm-inline",
"@cipherstash/auth/cookies": "npm:@cipherstash/auth@^0.41/cookies"
}
}
```

Provide the `CS_*` credentials when you serve the function — locally via an env file:

```bash
supabase functions serve --env-file ./supabase/functions/.env.local
```

```sh title="supabase/functions/.env.local"
CS_WORKSPACE_CRN=crn:ap-southeast-2.aws:<workspace-id>
CS_CLIENT_ID=...
CS_CLIENT_KEY=...
CS_CLIENT_ACCESS_KEY=...
```

For deployed functions, set the same values as [Edge Function secrets](https://supabase.com/dashboard/project/_/settings/functions).

## Encrypt and decrypt in a function

Each invocation builds a fresh client, but the CipherStash service token is cached in an HTTP-only cookie via `cookieStore`, so only the first request per session pays the full round-trip to CipherStash:

```typescript title="supabase/functions/encrypt/index.ts"
import { Encryption, AccessKeyStrategy, encryptedTable, encryptedColumn } from "@cipherstash/stack/wasm-inline"
import { cookieStore } from "@cipherstash/auth/cookies"

const patients = encryptedTable("patients", {
email: encryptedColumn("email").equality(),
})

Deno.serve(async (req) => {
const responseHeaders = new Headers({ "content-type": "application/json" })

// Cache the CipherStash token across invocations in an HTTP-only cookie
const authStrategy = AccessKeyStrategy.create(
Deno.env.get("CS_WORKSPACE_CRN")!,
Deno.env.get("CS_CLIENT_ACCESS_KEY")!,
{ store: cookieStore({ request: req, responseHeaders }) },
)
if (authStrategy.failure) {
return Response.json({ error: authStrategy.failure.type }, { status: 500, headers: responseHeaders })
}

const client = await Encryption({
schemas: [patients],
config: {
authStrategy: authStrategy.data,
clientId: Deno.env.get("CS_CLIENT_ID")!,
clientKey: Deno.env.get("CS_CLIENT_KEY")!,
},
})

const encrypted = await client.encrypt("alice@example.com", { column: patients.email, table: patients })
const decrypted = await client.decrypt(encrypted.data)

return Response.json({ ok: decrypted.data === "alice@example.com" }, { headers: responseHeaders })
})
```

Note the `.failure` / `.data` result shape: on the wasm entry, `AccessKeyStrategy.create` returns a `Result` you unwrap (unlike the Node build, where it returns the strategy directly).

## Per-user identity on the edge

To encrypt as the signed-in Supabase user rather than a service credential, swap `AccessKeyStrategy` for `OidcFederationStrategy` — its `getJwt` callback returns the current Supabase session token. The mechanics and the identity-lock layer are covered in [Supabase Auth](/integrations/supabase/auth); the only edge-specific difference is the `wasm-inline` import and the `Result` unwrap:

```typescript
import { OidcFederationStrategy } from "@cipherstash/stack/wasm-inline"
import { cookieStore } from "@cipherstash/auth/cookies"

const authStrategy = OidcFederationStrategy.create(
Deno.env.get("CS_WORKSPACE_CRN")!,
() => getSupabaseSessionToken(req), // return the current session JWT
{ store: cookieStore({ request: req, responseHeaders }) },
)
```

## Caveats

- **Cold start.** The first request pays the full path: WebAssembly compile, auth round-trip to CipherStash, and ZeroKMS dataset-key initialization. Subsequent requests reuse the cookie-cached token and only pay for the encrypt/decrypt work.
- **Bundle size.** The inline wasm build is larger than a native binding (the module ships as base64 in the JS). That's the trade for zero runtime config — acceptable for an edge function that boots once per worker.
- **Server-side only.** These credentials and the session token must never reach the browser. Keep this code in the Edge Function.

## Where to next

<Cards>
<Card title="Supabase Auth" href="/integrations/supabase/auth">
Federation and identity-locked encryption — the full identity story.
</Card>
<Card title="Quickstart" href="/integrations/supabase/quickstart">
The standard (Node) setup with the encryptedSupabase wrapper.
</Card>
<Card title="Stack reference" href="/reference/stack">
The core Encryption client API used here.
</Card>
</Cards>
Loading