Skip to content

Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference#39

Open
coderdan wants to merge 15 commits into
v2from
cip-3328-supabase
Open

Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference#39
coderdan wants to merge 15 commits into
v2from
cip-3328-supabase

Conversation

@coderdan

@coderdan coderdan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Supabase integration section, plus the canonical encryptedSupabaseV3 reference. Targets the v2 docs branch.

Originally written against encryptedSupabase (the EQL v2 dialect). Reworked onto encryptedSupabaseV3 and the EQL 3.0.0 domains, verified against the adapter source on feat/eql-v3-text-search-schema and against the EQL v3 domain SQL rather than the PR descriptions, which have drifted.

Pages

  • /reference/stack/supabase — the canonical wrapper page. await encryptedSupabaseV3(url, key, options?) or (client, options?), connect-time introspection, .from(tableName), optional schemas for types and startup verification, the full builder surface, the domain-to-term table, response and error shapes, and the deferred-execution model. The v2 wrapper is documented at the foot.
  • /integrations/supabase — section overview.
  • /integrations/supabase/quickstart — empty project to encrypted, searchable table: install EQL, type the columns, wrap the client, query.
  • /integrations/supabase/fundamentals — migration vs SQL Editor install, role grants, functional indexes, RLS composition, pooler and introspection connections.
  • /integrations/supabase/auth — Supabase Auth session to OidcFederationStrategy, then .withLockContext().
  • /integrations/supabase/orms — PostgREST path vs a direct Postgres connection.
  • /integrations/supabase/edge-functions — the wasm-inline core client on the Edge runtime.

What the rework changed

  • Async constructor, because it introspects the database and derives each column's config from its EQL domain. No encryptedTable to keep in sync, and .from() takes only a table name.
  • Introspection needs a direct Postgres connection (DATABASE_URL or options.databaseUrl), a different credential from the anon key. Added to the quickstart env block.
  • schemas is optional: compile-time types plus startup verification, imported from @cipherstash/stack/eql/v3. There is no @cipherstash/stack/schema/v3 export.
  • select('*') works on v3, expanded from the introspected column list. It throws on v2.
  • Column types are the prefixed domains, public.eql_v3_text_eq, not eql_v3.text_eq. Domains live in public; functions and operators live in eql_v3.
  • Supabase runs the EQL installer as a non-superuser, so it cannot create the block-ORE operator class and disables the ORE-backed domains, which raise on first write. Called out on the pages that offer the choice.
  • .like() / .ilike() throw on encrypted columns. Free-text search is .contains().
  • The query builder's .withLockContext() takes a LockContext instance; the core client's takes a plain { identityClaim } object. The auth page was passing the core shape.

Known defect, documented

.contains() cannot match substrings today: text_search forces include_original: true, so the needle's bloom carries the whole needle as an extra token that the haystack's bloom cannot contain. Tracked in cipherstash/stack#610. A warn callout describes the behaviour; remove it when the fix lands.

Depends on

Notes for review

  • Anti-drift rule respected: EQL mechanics live in /reference/eql/*; these pages link rather than restate.
  • edge-functions.mdx still uses the v2 schema builders, because @cipherstash/stack/wasm-inline exports encryptedTable / encryptedColumn from @/schema and does not export the v3 types. Left alone; it already carries a "being confirmed" callout. Worth deciding whether the edge build should re-export the v3 surface.
  • Unverified claim carried over: that custom domain types don't appear in the Table Editor's type picker, so the SQL Editor is recommended. Needs a check against a live project.
  • Placeholders for forward-linked IA URLs still contain Linear URLs. Those are pre-existing across the v2 branch and are tracked separately; they must be stripped before anything reaches production.
  • bun run build passes. validate-content, validate-links, validate-mermaid pass.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
public-docs Ready Ready Preview, 💬 4 unresolved
✅ 5 resolved
Jul 9, 2026 2:43pm

Request Review

@coderdan coderdan marked this pull request as ready for review July 2, 2026 12:56
…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)
Reorganize /integrations/supabase from 4 pages into: Overview (front
door from the marketplace listing), Quickstart (the tutorial, OAuth
folded in as optional), Fundamentals (database.mdx + dashboard-
experience.mdx merged), Data layer (ORM/framework router), Auth
(rebuilt on OIDC federation), and Edge Functions (new).

- Auth: replace the deprecated LockContext.identify() flow with
  OidcFederationStrategy as config.authStrategy, then
  .withLockContext({ identityClaim }) — lock context now correctly
  documented as requiring federation.
- Data layer: document the PostgREST vs direct-Postgres fork and
  Supabase pooler/connection glue; link out to canonical ORM pages
  rather than duplicating them.
- Edge Functions: written on the @cipherstash/stack/wasm-inline
  surface; wrapper-on-edge question gated behind a callout.
- Add Prisma + Next.js integration placeholders so Data-layer links
  resolve; fix stale /supabase/database link in the stack reference.
- Reorder meta.json; remove merged pages.
Addresses Vercel preview comment: the section folder and the index
page were both labelled 'Supabase', so the sidebar read 'Supabase ›
Supabase'. Title the index page 'Overview' to remove the repeat.
Replace the "How it fits together" section on the Supabase overview with
launch-article "How it works" copy. Fix EQL domain names against eql_v3
(int_ord -> integer_ord) and reframe text_search match as containment
matching (like LIKE '%term%'), with a caveat callout. Add Table Editor
screenshot.
- Remove em-dashes across the overview (AI "tell")
- Backtick `base` in "EQL base types"
- Convert the text_search note to a concise <Callout>
- Collapse the RLS/stack/KMS/performance/security sections into one
  linked "Built for the Supabase stack" summary
Add a shared, product-wide FAQ component (src/components/faq) that renders
an accordion and emits FAQPage JSON-LD. Generic answers (privacy, scaling,
KMS, RLS-vs-encryption, free tier) live once in faq/shared and are composed
with Supabase-specific questions inline on the overview. De-dupe the
performance/privacy prose now covered by the FAQ.
Reuse the shared privacy/KMS/free-tier FAQ entries and add DynamoDB-specific
questions (equality-only querying, keeping AWS SDK control, adoption).
Omits the Postgres-oriented scaling/RLS entries, which do not apply.
coderdan added 2 commits July 9, 2026 23:44
…ains

The Supabase pages documented `encryptedSupabase`, the EQL v2 dialect.
Stack 1.0 ships `encryptedSupabaseV3` alongside it, and it is a different
shape. Verified against the adapter source on the release branch, not
against the PR descriptions, which have drifted.

- `await encryptedSupabaseV3(url, key, options?)` or `(client, options?)`.
  It is async because it introspects the database at connect time, reads
  the EQL domain of every column, and derives the encryption config from
  it. There is no schema to declare and no `encryptedTable` to keep in
  sync, so `.from()` takes only a table name.
- Introspection needs a direct Postgres connection: `DATABASE_URL`, or
  `options.databaseUrl`. Documented in the quickstart env block.
- `schemas` is optional, and buys compile-time types plus startup
  verification. Import from `@cipherstash/stack/eql/v3`.
- `select('*')` works, expanded from the introspected column list. On v2
  it throws.
- `.like()` / `.ilike()` throw on encrypted columns. Free-text search is
  `.contains()`. With the default match options the query term carries
  the whole query string as a token, so a substring matches nothing;
  called out where it will bite.
- Column types are the EQL 3.0.0 prefixed domains: `public.eql_v3_text_eq`,
  not `eql_v3.text_eq`. The domains live in `public`; only the functions
  and operators live in `eql_v3`.
- Supabase runs the installer as a non-superuser, so the ORE-backed
  domains are disabled and raise on first write. Say so on the pages that
  offer the choice, and point at `_ord` / `text_search`.
- The query builder's `.withLockContext()` takes a `LockContext`
  instance, unlike the core client's, which takes a plain object. The
  auth page passed the wrong one.

The v2 wrapper is unchanged and still documented, at the foot of the
reference page.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed two commits: a v2 sync merge, and a rework of the Supabase pages onto encryptedSupabaseV3 + the EQL 3.0.0 domains.

Everything below was verified against the adapter source on feat/eql-v3-text-search-schema (stack #588 merged into it, not into main) and against the EQL v3 domain SQL. The PR bodies have drifted from the code in three places, so I did not use them:

  • stack #588's body shows .like("email", "%@example.com") working. A later commit on the same branch, feat(stack)!: replace like/ilike with contains on the v3 supabase surface, makes .like() / .ilike() throw on encrypted columns.
  • EQL #389's body says text_search keeps [hm, ob, bf]. Its generated schema says [hm, op, bf].
  • main's encryptedSupabaseV3 is a sync ({encryptionClient, supabaseClient}) with a two-argument from(). That is the pre-#588 shape, because #588's base is #535, which is still open.

What changed

  • await encryptedSupabaseV3(url, key, options?) or (client, options?). Async, because it introspects at connect time and derives each column's config from its EQL domain. .from() takes only a table name; no encryptedTable to keep in sync.
  • Introspection needs a direct Postgres connection (DATABASE_URL or options.databaseUrl), which is a different credential from the anon key. Added to the quickstart env block, since it is easy to miss.
  • schemas is optional: compile-time types plus startup verification. Imported from @cipherstash/stack/eql/v3 (there is no @cipherstash/stack/schema/v3 export).
  • select('*') works on v3, expanded from the introspected column list. It throws on v2.
  • Column types are the prefixed domains, public.eql_v3_text_eq, not eql_v3.text_eq. Domains live in public; functions and operators live in eql_v3.
  • Supabase runs the EQL installer as a non-superuser, so it cannot create the block-ORE operator class and disables the ORE-backed domains, which then raise feature_not_supported on first write. Called out on the pages that offer the choice.
  • The query builder's .withLockContext() takes a LockContext instance; the core client's takes a plain { identityClaim } object. The auth page was passing the core shape to the builder. (Our own validate-content guard caught this, which is a nice result for chore(ci): gate the build on deprecated and non-existent API in docs #61.)

Two things worth a second opinion

.contains() may be unusable for substring search. text_search always builds its match index with include_original: true and exposes no tuning. The query term therefore carries the whole query string as one of its tokens, and containment requires every query token to be present in the stored set, so contains("email", "alice") finds nothing on a stored alice@example.com. The adapter's own note in types.ts says as much: "include_original: true is therefore always in force, so a substring contains matches nothing on either." I documented that behaviour in a warn callout, but if it is a bug rather than the intent, the callout should become a bug report.

edge-functions.mdx still uses the v2 schema builders, because @cipherstash/stack/wasm-inline exports encryptedTable / encryptedColumn from @/schema and does not export the v3 types. I left the page alone; it already carries a "being confirmed" callout. Worth deciding whether the edge build should re-export the v3 surface.

Depends on

  • The v3 domain registry re-prefix (in progress) — until it lands, encryptedSupabaseV3 rejects every EQL 3.0.0 table, because DOMAIN_REGISTRY is keyed on the old unprefixed names.
  • stack #609, for .in() on encrypted columns and .not(col, 'in', […]), both of which the filter table documents as working.
  • docs(eql): update the reference to the EQL 3.0.0 shapes #67, which moves the EQL reference to these same domain names.

Checks

bun run build passes. validate-content, validate-links, validate-mermaid pass.

The operand is a storage payload, not a query term: PostgREST cannot cast
a filter value to the narrow _query domain, and protect-ffi 0.28 rejects
encryptQuery for the v3 scalars. So the needle's bloom carries the whole
needle as an include_original token, which the haystack's bloom cannot
contain unless the two are equal, or the needle is exactly one trigram
long.

Says it is a defect to be fixed in EQL rather than intended behaviour.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my note above on contains. My explanation was wrong; the conclusion happened to be right, for a different reason. Pushed 6a16f79 to fix the callouts.

You're right about the storage side. include_original means the stored value's bloom carries its trigrams and the whole value as one extra token, so a query matches on the exact original term or on any of its trigrams. That is the intended widening.

What I missed is which payload the wrapper puts on the query side. It sends a full storage payload as the operand, not a narrow query term, and the source says why:

PostgREST has no syntax to cast a filter VALUE, and an uncast literal is ambiguous between the _query and jsonb @>/= overloads (42725). […] Independently, protect-ffi 0.28 throws EQL_V3_QUERY_UNSUPPORTED for any v3 scalar encryptQuery, so a narrowed term cannot be produced today.

Confirmed at query-builder-v3.ts:356, which calls encryptionClient.encrypt(...) rather than encryptQuery(...).

So the needle gets tokenized as if it were a stored value: trigrams("alice") plus include_original("alice"). Containment requires every needle bit to be present in the haystack. The trigrams are there, but the whole-needle token "alice" is not among the haystack's tokens, which are trigrams("alice@example.com") plus "alice@example.com". So containment fails.

It follows that .contains() matches only when the needle equals the stored value, or when the needle is exactly three characters, where its include_original token is one of the haystack's trigrams.

The class doc reaches the same conclusion and calls it a defect:

KNOWN BROKEN for real substrings, and not fixable from this file. […] v3 Drizzle's contains has the same defect for the same reason. Tracked in EQL; do not paper over it here.

The docs now describe it as a known defect with that precise rule, rather than as intended behaviour. I could not find the tracking issue in encrypt-query-language (searched open and closed), so it may be in Linear.

Drizzle and the Supabase wrapper share the defect but not the cause. The
common blocker is that protect-ffi has no v3 scalar encryptQuery, so no
client can build a narrow operand. Only Supabase is additionally blocked
by PostgREST's inability to cast a filter value to the query domain;
Drizzle emits its own SQL and can cast once a narrow term exists.
Not a protect-ffi gap: substring containment works at the ffi layer when
include_original is off, which is the schema default. stack's shared
match defaults force it on, inherited from the v2 builder, where QueryMode
dropped the original token. Link the tracking issue.
@coderdan coderdan changed the title Docs V2: Supabase integration section + canonical encryptedSupabase reference (CIP-3328) Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference Jul 9, 2026
Docs do not link GitHub issues. The callout had also accumulated two
competing explanations across revisions; keep the one that is true.
…w link

- Remove the `.contains()` substring callout from the reference page and
  the quickstart. Known defects belong in the issue tracker, not in the
  docs; the fix is imminent.
- Remove the pre-publish redirect that sent /integrations/supabase to the
  legacy /stack/cipherstash/supabase page. It was placed on main so the
  future URL would resolve before v2 shipped, and its own comment says to
  remove it once v2 owns the URL. This branch is where v2 owns it, so the
  section's Overview nav item was landing on the old IA.
- List Supabase first under Integrations.
The screenshot no longer reflects the product, and there is no
replacement yet. It also illustrated adding EQL columns through the
Table Editor, which Fundamentals already says is unreliable: the
type picker does not surface custom domains. The copy now sends
readers to SQL, which is the dependable path and what a migration
needs anyway.

Also drops the last two "Supabase Studio" mentions. It is the
Supabase Dashboard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant