Skip to content

Commit 9683c86

Browse files
committed
docs(v2): Supabase integration section + canonical encryptedSupabase reference (CIP-3328)
- /reference/stack/supabase: THE canonical encryptedSupabase page — one signature ({encryptionClient, supabaseClient}, .from(table, schema)), full query-builder surface, schema-capability → EQL-term mapping, response/error shapes, deferred-execution model - /integrations/supabase: flagship tutorial rewritten on EQL v3 (install script → eql_v3 typed columns → wrapper → queries incl. ORDER BY on _ord; free-text as token containment, no LIKE) - /integrations/supabase/{database,auth,dashboard-experience}: migrations-vs-SQL-Editor install, grants, indexes, RLS composition; Supabase Auth JWT → LockContext; Table Editor expectations + OAuth integration flow - placeholder pages for 6 forward-linked IA URLs (identity-aware- encryption, schema-design, encrypt-existing-data, drizzle, audit-logging, cts) so no internal link 404s - IA.md: tick CIP-3328 items; add CIP-3355 SDK-alignment gate (wrapper wire behaviour is still EQL v2 — blocks final merge)
1 parent 19a132b commit 9683c86

13 files changed

Lines changed: 782 additions & 15 deletions

File tree

IA.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).
4343

4444
- [x] Section scaffold 🚧 (index + supabase stub with facet exemplar)
4545
- [ ] `/integrations` index — category grid w/ setup badges
46-
- [ ] `/integrations/supabase` — flagship tutorial (CIP-3328)
47-
- [ ] `/integrations/supabase/database`
48-
- [ ] `/integrations/supabase/auth`
49-
- [ ] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema
46+
- [x] `/integrations/supabase` — flagship tutorial (CIP-3328)
47+
- [x] `/integrations/supabase/database`
48+
- [x] `/integrations/supabase/auth`
49+
- [x] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema
5050
- [ ]`/integrations/supabase/edge-functions` — pending Deno/FFI answer
5151
- [ ]`/integrations/supabase/realtime` — pending product verification
52-
- [ ] `/integrations/drizzle` — merge the two divergent Drizzle pages
52+
- [ ] `/integrations/drizzle` 🚧 — merge the two divergent Drizzle pages
5353
- [ ] `/integrations/prisma-next`
5454
- [ ] `/integrations/aws/rds-aurora` — Proxy path
5555
- [ ] `/integrations/aws/dynamodb`
@@ -155,7 +155,7 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).
155155
- [ ] `/reference/stack` — client + configuration (port encryption/* pages)
156156
- [ ] `/reference/stack/schema`
157157
- [ ] `/reference/stack/encrypt-decrypt` (+ bulk, models)
158-
- [ ] `/reference/stack/supabase` — THE canonical `encryptedSupabase` page, ONE signature (CIP-3328)
158+
- [x] `/reference/stack/supabase` — THE canonical `encryptedSupabase` page, ONE signature (CIP-3328)
159159
- [ ] `/reference/stack/drizzle-operators`
160160
- [ ] `/reference/stack/errors` — port error-handling; miette catalog later (CIP-3338)
161161
- [ ] `/reference/stack/upgrading-from-protect` (retitled package-rename guide)
@@ -188,5 +188,9 @@ live at `/docs/errors/<code>` — permanent, never restructured (CIP-3338).
188188
documents the release as decided, ahead of the eql_v3 branch: payload `v: 3`,
189189
OPE SEM specifier, Docker tag `:17-3.0.0`, `version()` output, schema files.
190190
Each must land upstream or be walked back in the docs before merge
191+
- [ ] ⛔ Stack SDK Supabase-wrapper v3 alignment (CIP-3355, blocks CIP-3335) — the
192+
Supabase section documents the 0.18 wrapper API with v3 wire semantics; the
193+
wrapper itself is still v2 (composite type, `like` wire op, v2 payloads) and
194+
the SDK's v3 branches don't touch `src/supabase/` yet
191195
- [ ] Flip `ENABLE_V2_REDIRECTS=1`, delete `content/stack` + `/stack` routes + legacy loader (CIP-3335)
192196
- [ ] Consistency sweep + Supabase listing v3 revision (CIP-3335)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Identity-aware encryption
3+
description: "Lock contexts and CTS: binding encrypted values to a user identity so only that identity can decrypt them."
4+
type: concept
5+
---
6+
7+
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).
8+
9+
Until it lands, the current version lives in the [existing docs](/stack/cipherstash/encryption/identity).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Schema design
3+
description: "Choosing the right encrypted type and capability for each column."
4+
type: guide
5+
---
6+
7+
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).
8+
9+
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.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Encrypt existing data
3+
description: "Backfilling encryption onto live tables, column by column."
4+
type: guide
5+
---
6+
7+
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).
8+
9+
Until it lands, the current version lives in the [existing docs](/stack/cipherstash/proxy/encrypt-tool).
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: Drizzle
3+
description: "Encrypted columns with Drizzle ORM."
4+
type: tutorial
5+
integration:
6+
category: orm
7+
setup: code-only
8+
pairsWith: [supabase, nextjs]
9+
---
10+
11+
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).
12+
13+
Until it lands, the current version lives in the [existing docs](/stack/cipherstash/encryption/drizzle).
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
title: Supabase Auth
3+
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."
4+
type: guide
5+
components: [encryption, auth]
6+
audience: [developer]
7+
verifiedAgainst:
8+
stack: "0.18.0"
9+
---
10+
11+
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.
12+
13+
With Supabase Auth, the identity is the session JWT you already have.
14+
15+
<Callout type="warn">
16+
Lock contexts require a Business or Enterprise workspace plan.
17+
</Callout>
18+
19+
## Register Supabase Auth with your workspace
20+
21+
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:
22+
23+
```
24+
https://<project-ref>.supabase.co/auth/v1
25+
```
26+
27+
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).
28+
29+
## Lock queries to the session user
30+
31+
Identify the user with their Supabase session token, then attach the lock context to any [`encryptedSupabase`](/reference/stack/supabase) query:
32+
33+
```typescript
34+
import { LockContext } from "@cipherstash/stack/identity"
35+
import { db } from "./lib/db"
36+
import { patients } from "./lib/schema"
37+
38+
// 1. The Supabase session you already have
39+
const { data: { session } } = await supabaseClient.auth.getSession()
40+
41+
// 2. Identify: exchanges the Supabase JWT for a CTS identity token
42+
const lc = new LockContext()
43+
const identified = await lc.identify(session.access_token)
44+
45+
if (identified.failure) {
46+
throw new Error(identified.failure.message)
47+
}
48+
const lockContext = identified.data
49+
50+
// 3. Encrypt and decrypt under that identity
51+
await db.from("patients", patients)
52+
.insert({ email: "alice@example.com", name: "Alice Chen" })
53+
.withLockContext(lockContext)
54+
55+
const { data } = await db.from("patients", patients)
56+
.select("id, email, name")
57+
.eq("email", "alice@example.com")
58+
.withLockContext(lockContext)
59+
```
60+
61+
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.
62+
63+
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:
64+
65+
```typescript
66+
const lc = new LockContext({
67+
context: { identityClaim: ["sub"] }, // default; add "scopes" to bind permissions too
68+
})
69+
```
70+
71+
## Where it fits
72+
73+
- **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.
74+
- **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.
75+
- **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.
76+
77+
## Errors
78+
79+
`identify()` returns a result, not a throw. The common failures:
80+
81+
| Scenario | What you see |
82+
| --- | --- |
83+
| Expired or invalid Supabase JWT | `CtsTokenError` from `identify()` |
84+
| Issuer not registered with the workspace | `CtsTokenError` — register the OIDC issuer first |
85+
| Expired CTS token at query time | `LockContextError` — call `identify()` again |
86+
| Decrypting another user's locked value | `encryptionError` on the [query response](/reference/stack/supabase#responses-and-errors) |
87+
88+
## Where to next
89+
90+
<Cards>
91+
<Card title="Identity-aware encryption" href="/concepts/identity-aware-encryption">
92+
The concept: lock contexts, CTS, and what identity binding proves.
93+
</Card>
94+
<Card title="encryptedSupabase reference" href="/reference/stack/supabase">
95+
`.withLockContext()` and `.audit()` on the query builder.
96+
</Card>
97+
<Card title="Auth reference" href="/reference/auth">
98+
OIDC configuration, access keys, and clients.
99+
</Card>
100+
</Cards>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: Dashboard experience
3+
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."
4+
type: guide
5+
components: [eql]
6+
audience: [developer]
7+
verifiedAgainst:
8+
eql: "3.0.0"
9+
---
10+
11+
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.
12+
13+
## Table Editor
14+
15+
Encrypted columns show **ciphertext payloads**, not plaintext. A `patients` row looks like:
16+
17+
| id | email | name | plan |
18+
| --- | --- | --- | --- |
19+
| 1 | `{"v": 3, "i": {"t": "patients", "c": "email"}, "c": "mBbKmsMM%bK#…", "hm": "9c8ec1d2…"}` | `{"v": 3, "i": …, "c": "x7Qq…", "bf": [42, 1290, …]}` | `standard` |
20+
21+
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.
22+
23+
Practical consequences:
24+
25+
- **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.
26+
- **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.
27+
- **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.
28+
29+
## Creating encrypted columns
30+
31+
Use the **SQL Editor** for schema work on encrypted columns:
32+
33+
```sql
34+
ALTER TABLE patients ADD COLUMN tax_id eql_v3.text_eq;
35+
```
36+
37+
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)).
38+
39+
## SQL Editor
40+
41+
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:
42+
43+
```sql
44+
-- Is EQL installed, and which version?
45+
SELECT eql_v3.version();
46+
47+
-- Which columns are encrypted?
48+
SELECT table_name, column_name, udt_name
49+
FROM information_schema.columns
50+
WHERE udt_schema = 'eql_v3';
51+
```
52+
53+
## API settings
54+
55+
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.
56+
57+
## Connecting your project to CipherStash
58+
59+
The CipherStash dashboard has a first-class Supabase integration at [dashboard.cipherstash.com](https://dashboard.cipherstash.com):
60+
61+
<Steps>
62+
<Step>
63+
### Connect via OAuth
64+
65+
Authorize CipherStash against your Supabase organization. The integration requests read-only scopes (`projects:read`, `database:read`) — it never sees your database secrets.
66+
</Step>
67+
<Step>
68+
### Pick a project and run the readiness checks
69+
70+
The setup hub verifies EQL is installed (`eql_v3.version()`), checks for encrypted columns, and flags anything missing.
71+
</Step>
72+
<Step>
73+
### Configure identity (optional)
74+
75+
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).
76+
</Step>
77+
<Step>
78+
### Copy your environment
79+
80+
The hub emits the `CS_*` credentials block for your app's environment, ready to pair with your existing `SUPABASE_URL` and keys.
81+
</Step>
82+
</Steps>
83+
84+
## Where to next
85+
86+
<Cards>
87+
<Card title="Supabase tutorial" href="/integrations/supabase">
88+
The end-to-end setup this page supports.
89+
</Card>
90+
<Card title="Database setup" href="/integrations/supabase/database">
91+
Grants, migrations, and indexes in detail.
92+
</Card>
93+
<Card title="EQL core concepts" href="/reference/eql/core-concepts">
94+
What's actually inside those ciphertext payloads.
95+
</Card>
96+
</Cards>

0 commit comments

Comments
 (0)