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 @@ -43,13 +43,13 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).

- [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 @@ -155,7 +155,7 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).
- [ ] `/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 @@ -188,5 +188,9 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).
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).
100 changes: 100 additions & 0 deletions content/docs/integrations/supabase/auth.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
title: Supabase Auth
description: "Lock decryption to the authenticated user: use the Supabase Auth session JWT as a lock context so encrypted data only decrypts for the identity it belongs to."
type: guide
components: [encryption, auth]
audience: [developer]
verifiedAgainst:
stack: "0.18.0"
---

Row Level Security scopes *queries* to the logged-in user. Identity-aware encryption goes one layer deeper: values encrypted with a **lock context** can only be *decrypted* by presenting the same user's identity — enforced by [CTS](/security/cts), CipherStash's token service, not by your application code. Even your own backend, holding valid workspace credentials, cannot decrypt another user's locked values without that user's session.

With Supabase Auth, the identity is the session JWT you already have.

<Callout type="warn">
Lock contexts require a Business or Enterprise workspace plan.
</Callout>

## Register Supabase Auth with your workspace

CTS needs to trust your Supabase project as an OIDC issuer — that's what lets it exchange a Supabase session JWT for a CipherStash identity token. The issuer for a Supabase project is:

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

The easiest path is the CipherStash dashboard's [Supabase integration](/integrations/supabase/dashboard-experience), which configures this during setup. Manual OIDC configuration is covered in the [auth reference](/reference/auth).

## Lock queries to the session user

Identify the user with their Supabase session token, then attach the lock context to any [`encryptedSupabase`](/reference/stack/supabase) query:

```typescript
import { LockContext } from "@cipherstash/stack/identity"
import { db } from "./lib/db"
import { patients } from "./lib/schema"

// 1. The Supabase session you already have
const { data: { session } } = await supabaseClient.auth.getSession()

// 2. Identify: exchanges the Supabase JWT for a CTS identity token
const lc = new LockContext()
const identified = await lc.identify(session.access_token)

if (identified.failure) {
throw new Error(identified.failure.message)
}
const lockContext = identified.data

// 3. Encrypt and decrypt under that identity
await db.from("patients", patients)
.insert({ email: "alice@example.com", name: "Alice Chen" })
.withLockContext(lockContext)

const { data } = await db.from("patients", patients)
.select("id, email, name")
.eq("email", "alice@example.com")
.withLockContext(lockContext)
```

A value written with `.withLockContext()` can only be decrypted with a lock context for the **same identity**. Reading it without one — or as a different user — fails with an encryption error, regardless of what RLS allows.

By default the identity is the JWT's `sub` claim, which for Supabase Auth is the user's UUID. You can widen or change that:

```typescript
const lc = new LockContext({
context: { identityClaim: ["sub"] }, // default; add "scopes" to bind permissions too
})
```

## 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. Plain encryption already protects it from the database side; RLS scopes who can query it.
- **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

`identify()` returns a result, not a throw. The common failures:

| Scenario | What you see |
| --- | --- |
| Expired or invalid Supabase JWT | `CtsTokenError` from `identify()` |
| Issuer not registered with the workspace | `CtsTokenError` — register the OIDC issuer first |
| Expired CTS token at query time | `LockContextError` — call `identify()` again |
| Decrypting another user's locked value | `encryptionError` on the [query response](/reference/stack/supabase#responses-and-errors) |

## Where to next

<Cards>
<Card title="Identity-aware encryption" href="/concepts/identity-aware-encryption">
The concept: lock contexts, CTS, and what identity binding proves.
</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 configuration, access keys, and clients.
</Card>
</Cards>
96 changes: 96 additions & 0 deletions content/docs/integrations/supabase/dashboard-experience.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: Dashboard experience
description: "What encrypted columns look like across the Supabase dashboard — Table Editor, SQL Editor, API settings — and how to connect your project to CipherStash via OAuth."
type: guide
components: [eql]
audience: [developer]
verifiedAgainst:
eql: "3.0.0"
---

Encrypted columns live inside your normal Supabase dashboard — no separate tool. This page sets expectations for what you'll see in each part of the dashboard, and covers the CipherStash-side integration that connects your project.

## Table Editor

Encrypted columns show **ciphertext payloads**, not plaintext. A `patients` row looks like:

| id | email | name | plan |
| --- | --- | --- | --- |
| 1 | `{"v": 3, "i": {"t": "patients", "c": "email"}, "c": "mBbKmsMM%bK#…", "hm": "9c8ec1d2…"}` | `{"v": 3, "i": …, "c": "x7Qq…", "bf": [42, 1290, …]}` | `standard` |

This is the point, not a limitation: what the Table Editor shows is exactly what a backup, a replication stream, a leaked `service_role` key, or a curious operator sees. The payload structure (`v`, `i`, `c`, and the index terms) is documented in [EQL core concepts](/reference/eql/core-concepts) — none of it reveals the plaintext.

Practical consequences:

- **Reading data** happens through your app (or any client using the [Stack SDK](/reference/stack)); the dashboard can't decrypt, because the keys never go near it.
- **Hand-editing an encrypted cell** in the Table Editor will produce a value that fails the column's `CHECK` constraint or fails to decrypt — treat encrypted columns as read-only in the dashboard.
- **Filtering and sorting** on encrypted columns in the Table Editor UI operates on the raw payloads, so it won't return meaningful results. Query through your app instead.

## Creating encrypted columns

Use the **SQL Editor** for schema work on encrypted columns:

```sql
ALTER TABLE patients ADD COLUMN tax_id eql_v3.text_eq;
```

The `eql_v3` domain types are user-defined types, and the Table Editor's column-type picker doesn't reliably surface custom domains — SQL is the dependable path, and it's what your migrations should contain anyway (see [Database setup](/integrations/supabase/database)).

## SQL Editor

Everything in the [EQL reference](/reference/eql) can be run from the SQL Editor — installing EQL, creating indexes, `EXPLAIN`-ing query plans. Two checks worth knowing:

```sql
-- Is EQL installed, and which version?
SELECT eql_v3.version();

-- Which columns are encrypted?
SELECT table_name, column_name, udt_name
FROM information_schema.columns
WHERE udt_schema = 'eql_v3';
```

## API settings

Normal operation needs **no** API-settings changes: your tables are in the `public` schema, and the wrapper's queries are ordinary PostgREST calls. The `eql_v3` schema needs role *grants* (covered in [Database setup](/integrations/supabase/database#grants)), but does **not** need to be in the *Exposed schemas* list unless you want to call EQL functions directly over REST.

## Connecting your project to CipherStash

The CipherStash dashboard has a first-class Supabase integration at [dashboard.cipherstash.com](https://dashboard.cipherstash.com):

<Steps>
<Step>
### Connect via OAuth

Authorize CipherStash against your Supabase organization. The integration requests read-only scopes (`projects:read`, `database:read`) — it never sees your database secrets.
</Step>
<Step>
### Pick a project and run the readiness checks

The setup hub verifies EQL is installed (`eql_v3.version()`), checks for encrypted columns, and flags anything missing.
</Step>
<Step>
### Configure identity (optional)

One click registers your project's Supabase Auth issuer (`https://<project-ref>.supabase.co/auth/v1`) with your workspace, enabling [identity-locked encryption](/integrations/supabase/auth).
</Step>
<Step>
### Copy your environment

The hub emits the `CS_*` credentials block for your app's environment, ready to pair with your existing `SUPABASE_URL` and keys.
</Step>
</Steps>

## Where to next

<Cards>
<Card title="Supabase tutorial" href="/integrations/supabase">
The end-to-end setup this page supports.
</Card>
<Card title="Database setup" href="/integrations/supabase/database">
Grants, migrations, and indexes in detail.
</Card>
<Card title="EQL core concepts" href="/reference/eql/core-concepts">
What's actually inside those ciphertext payloads.
</Card>
</Cards>
Loading