Docs V2: Supabase integration and supabase-js guide - #39
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
9683c86 to
fd918a6
Compare
- 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
|
Pushed two commits: a Everything below was verified against the adapter source on
What changed
Two things worth a second opinion
Depends on
Checks
|
|
Correction to my note above on You're right about the storage side. 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:
Confirmed at So the needle gets tokenized as if it were a stored value: It follows that The class doc reaches the same conclusion and calls it a defect:
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 |
|
Updated to the shipped Stack 1.0-rc.0 family and EQL 3.0 defaults (commit The adapters split into their own packages.
EQL v3 is now the CLI default. agent-skills: the bundled skill set changed with 1.0 — Heads-up, not from these edits: |
|
Fixed the failing Vercel build (
Verified locally: This is a shared generator fix. #43 and #54 will hit the same red build until they get it — either after this merges to v2 and they rebase, or I can cherry-pick |
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.
…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.
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.
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.
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.
- Install EQL with `npx stash eql install --eql-version 3` rather than curling the release SQL into the SQL Editor or a migration file. The CLI detects Supabase from DATABASE_URL and installs directly. - Say that v3 is direct-install only: --migration, --drizzle, --latest and --migrations-dir are all rejected alongside --eql-version 3, so a v3 install does not survive `supabase db reset`. That retires the "commit it as your earliest migration" path, which only works for v2. - Auth: promote the server-side-only warning from a blockquote to a warn callout, and lead with the rule rather than the token-refresh detail. - Edge Functions: drop the "being confirmed" callout about the wrapper.
The page was an unwritten placeholder. Removes it, the "Next.js" card on the Supabase ORMs page, its two IA.md entries, and `nextjs` from every `pairsWith` facet. Also corrects `prisma-next` to `prisma` in the Supabase facets: the page is at /integrations/prisma, so the old value named nothing.
…on index Drizzle, on EQL v3. Concrete column types from `@cipherstash/stack/eql/v3/drizzle`, `extractEncryptionSchemaV3` + `EncryptionV3`, and the async capability-checked operators. Notes that `--eql-version 3` cannot emit a Drizzle migration, and that free-text search is `ops.contains` rather than `like`. Prisma, on EQL v2. The integration is `@cipherstash/prisma-next`, which targets Prisma Next rather than classic `@prisma/client`, and whose migrations create `eql_v2_encrypted` columns. Both facts are stated up front rather than left for a reader to discover. Classic Prisma users are pointed at the SDK. The index routes by how the application talks to the database, since that is what actually decides the integration. Also teaches validate-content-api about per-file exemptions: the Prisma page must name `eql_v2_encrypted` to be correct, and the v3-only rules would otherwise reject it. Verified the rule still fires elsewhere. Fixes that rule's own fix hint, which suggested the pre-3.0.0 `public.text_eq`.
The integration is `@cipherstash/prisma-next`, for the Prisma Next framework, not classic `@prisma/client`. Titling it "Prisma" invited exactly the confusion the page then had to spend a callout undoing. Moves the slug to /integrations/prisma-next, which is what IA.md already expected, and repoints the two inbound cards, the `pairsWith` facets, and the validate-content exemption path. Ticks the three integrations entries in IA.md, noting that Prisma Next is EQL v2 today and should be revisited when v3 support lands.
Stack 1.0 split the Drizzle and Supabase adapters into their own packages and the stash CLI made EQL v3 the default. Verified against the published 1.0.0-rc.0 tarballs. Import moves (the old subpath exports are removed from @cipherstash/stack): - @cipherstash/stack/supabase → @cipherstash/stack-supabase - @cipherstash/stack/eql/v3/drizzle → @cipherstash/stack-drizzle/v3 Install commands add the split packages; the Supabase reference gains a package-install note. CLI: `stash eql install` and `stash init` now target EQL v3 by default, so the explicit `--eql-version 3` is dropped everywhere. The get-started quickstart's separate "force v3 over the v2 install" step is removed — init installs v3 directly, exactly as that step's own callout predicted. The Drizzle-migration restriction (#613) still holds and stays documented. agent-skills: the bundled skill set changed. `stash-secrets` is gone (secrets removed from the SDK) and `stash-supply-chain-security` is new. Repointed the Drizzle/Supabase skills at their split packages and noted the Drizzle skill now covers the v3 dialect. verifiedAgainst bumped to 1.0.0-rc.0 on the pages re-checked against it. Note: `bun run build` is currently red at the TypeDoc generator, which clones the latest @cipherstash/stack source; that source needs a protect-ffi with the renamed ProtectError export. Pre-existing and unrelated to these content edits (reproduces with them stashed).
Stack renamed the encrypted free-text method in cipherstash/stack#617 (`feat(stack-drizzle,stack-supabase)!: rename encrypted contains() -> matches()`), landed 2026-07-13 — one day before this branch was last updated, so it was missed. The rename is not cosmetic. `contains()` now RAISES on an encrypted text column, because containment promises exactness that bloom-filter matching cannot deliver: contains() is native (exact) containment and does not apply to encrypted column "email". Use matches() for encrypted free-text search. Every free-text example on these pages taught the spelling that now raises. `contains()` is still correct for encrypted JSON (sub-document containment) and plaintext arrays, so the filter table now lists both, plus the selectorEq/selectorNe pair added alongside them. Also rewrites "There is no `.like()`", which was wrong in the other direction. `like`/`ilike` on an encrypted column do not throw — they are a compatibility shim that reduces the pattern to a needle and delegates to `matches()`. The result is approximate (case-insensitive, one-sided, anchoring lost) and only an unapproximable pattern — an internal `%`, or any `_` — raises. Plaintext columns keep real SQL LIKE. The section now says that, and the anchor is renamed to match, with the inbound link updated. Verified against @cipherstash/stack@1.0.0-rc.4 (packages/stack-supabase): encryptedSupabaseV3's four overloads are async and take (client | url+key) plus options, and databaseUrl/config/schemas are all real, so the surrounding examples were already correct. The v2 paragraph checks out too — encryptedSupabase is synchronous, takes { encryptionClient, supabaseClient }, and needs .from(table, schema). Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
… adapter Correcting my own earlier commit. Renaming contains() -> matches() fixed the spelling but documented an operation that does not run: on EQL 3.0.2+ the supabase-js wrapper raises for every encrypted free-text and encrypted-JSON term, whatever the spelling. [supabase v3]: matches() on encrypted column "email" is unavailable with EQL 3.0.2+: the SQL operator requires an eql_v3.query_* cast that PostgREST cannot express. Use the Drizzle or Prisma Next adapter, or a scoped SQL/RPC function. The docs pin EQL 3.0.4, so this is the behaviour every reader gets. The guard fires for the freeTextSearch and searchableJson query types via any spelling — matches(), contains() on a json column, selectorEq()/selectorNe(), and the same terms reached through not(), or(), or a raw filter() — and fails closed on an unknown EQL version. It does NOT touch equality, ordering, or range, so the rest of the page stands. Scoped to the supabase-js query path, which is what the limit is about. The Auth and Edge Functions pages are unaffected: this is PostgREST's inability to express an operand cast, not a limit on CipherStash with Supabase. - The wrapper reference leads the free-text section with the constraint and the three workarounds, and marks the affected rows in the filter table. - The quickstart drops its free-text example rather than showing a call that raises, and its callout now says which operations do work. - fundamentals' column-type table is accurate as written (these ARE the column's capabilities) but a reader could pick text_match or json and only then discover they cannot query it here, so it points at the constraint. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
…s remove_v2 Two Vercel toolbar comments on the preview. edge-functions: generate the CS_* values with `stash env` rather than having the reader assemble the file by hand. Additive — the explicit variable listing stays, since it is what documents which four the SDK actually reads. `--write` is a boolean (verified against stash@0.17.1), so the example redirects to the env file that `supabase functions serve --env-file` then consumes. fundamentals: rewrite the install callout against the stack `remove_v2` branch. What was out of date: - It justified the v3 restrictions as flags that "exist for the v2 install paths". With v2 removed there are no v2 install paths, and the install is always v3, so the framing goes. - It listed `--drizzle` alongside the other rejected flags. `--drizzle` has had its own v3 answer since stack#691: `stash eql migration --drizzle` generates a v3 install migration, cs_migrations tracking schema included, for Drizzle projects that want the install in their migration history. The install command still rejects the flag, but now points there rather than at `--eql-version 2`. - Also drops "`--eql-version 2` opts back to v2" from the prose above it. `--migration`, `--migrations-dir`, and `--latest` remain rejected, and there is still no Supabase migration variant — one v3 SQL artifact targets every platform — so the `supabase db reset` consequence stands and is kept. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
…stead `remove_v2` makes `encryptedSupabase` the v3 wrapper, so a section documenting it as "the EQL v2 dialect" would describe the opposite of what it names. The v2 dialect it described will never have existed for a reader of the live site, since these docs ship after that branch. Replaced with a short note on the naming, because the alias is the thing a reader actually needs: `encryptedSupabase` and `encryptedSupabaseV3` are the same function, either import works, and the V3 spelling is a deprecated alias kept for existing imports now that the suffix is redundant. Same for the type names. Also drops the "pick the dialect by function" framing — after the merge there is only one dialect. The examples on this page still say `encryptedSupabaseV3`, deliberately. That name is correct both today and after the merge, whereas `encryptedSupabase` means the OLD synchronous v2 wrapper on the currently published SDK (1.0.0-rc.4) — switching the examples now would document a different function with a different signature. They should be renamed once remove_v2 ships, along with this page's `verifiedAgainst`. Also fixes one cell in choose-your-stack, which is outside this PR's scope but factually wrong in the same way: it advertised `.like()` as something that "keeps working" on Supabase. It delegates to `matches()`, which raises on EQL 3.0.2+ through PostgREST. Swapped for `.order()`, which does. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
… Data layer away Four structural changes to the Integrations section: - The /integrations landing page leaves the sidebar (`"!index"` in meta.json, which drops it as both a child item and the folder link). It stays reachable from the docs index, Get started, and the v2 redirect map — it just isn't a nav entry. - The landing page becomes a plain list by category: Database (Supabase, Prisma Postgres) and ORM (Supabase-js, Prisma Next, Drizzle). The decision content it used to duplicate — the Postgres platform table, the ORM table, the identity-provider table — already lives on /get-started/choose-your-stack, which it now points at instead. - Supabase Overview and Fundamentals merge into one Overview page. The overlap was substantial: both explained EQL column types, both covered RLS, and both explained why the Table Editor can't create encrypted columns. Fundamentals' section anchors are preserved on the merged page, so #installing-eql, #row-level-security, #working-in-the-dashboard and #connecting-your-project-to-cipherstash keep resolving; inbound links across auth, quickstart and the wrapper reference are repointed. - Data layer folds into the Quickstart as "Using an ORM instead", condensed from 71 lines to ~20: what the direct-connection path is, which Supabase connection string to use, and the fact that a privileged direct connection bypasses RLS. Verified against the rendered output rather than the source: the built sidebar has no bare /integrations link, and every anchor the merge moved resolves to a real id on the merged page. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
`content/stack/reference/drizzle.mdx` points at `/integrations/drizzle`, which is a page this branch creates. #81 (the link-validator fix, off v2) removes that link, because on v2 alone it is a genuine 404 — the target does not exist there yet. That set up a silent failure. This branch did not touch the line, so rebasing onto a v2 containing #81 would have taken #81's removal without a conflict, dropping the cross-link even though the page it points at had just landed. Rewording the sentence here makes the branch an explicit editor of that line, so the two changes conflict and the merge has to make a decision instead of quietly picking one. The rewrite is also more accurate on its own terms: this page documents the older `@cipherstash/stack/drizzle` entry point, which the previous phrasing left implicit. The redirect itself needed no change — `/stack/reference/drizzle` → `/integrations/drizzle` has been in v2-redirects.mjs all along. What this branch supplies is the destination, so the redirect resolves here where it did not on v2. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
The quickstart went from an empty project to an encrypted table, which is the easy case and not the one readers arrive with. Reframe it around a `patients` table that already holds live plaintext data, and walk the deployment ladder from the stash-deployment skill: add nullable `*_encrypted` mirror columns for `name` and `date_of_birth`, dual-write everywhere, deploy, backfill the historical rows, build the extractor indexes, then cut reads over. Stops short of dropping the plaintext columns. That is a separate, irreversible deploy, so it gets a follow-up section covering the soak, `stash encrypt drop`, and why the generated migration re-checks coverage at apply time — with sequencing deferred to the deployment guide. Carries over the failure modes that lose data silently: nullable-mandatory mirrors, the `_encrypted` naming convention `backfill` resolves by default, dual-writing on every path, running the backfill under the app's `CS_*` credentials, and needing those credentials at build time. Keeps a callout for the greenfield path, and updates the overview's descriptions of the quickstart plus its migration FAQ answer to match.
5338fc2 to
59f8f57
Compare
Summary
Adds the complete Supabase integration journey to the V2 docs and aligns it with the Stack 1.0, EQL 3.0.4, and Auth 0.42 release APIs.
The section covers database setup and operations, migrating live plaintext columns without downtime, day-to-day supabase-js usage, Supabase Auth federation, identity-bound encryption, and Edge Functions. It also adds the canonical
encryptedSupabasequery-builder reference.Supabase pages
/integrations/supabase— overview of EQL installation, encrypted domain types, grants, indexes, RLS composition, connections, dashboard behavior, and the CipherStash dashboard integration./integrations/supabase/quickstart— converts populated plaintext columns using nullable encrypted mirrors, dual-writes, a production deploy gate, resumable backfill, functional indexes, read cutover, soak, and guarded plaintext removal./integrations/supabase/supabase-js— creates the encrypted client and covers optional typed schemas, CRUD, filters, ordering, pagination, responses, errors, RLS, audit metadata, and lock contexts./integrations/supabase/auth— federates the current Supabase Auth session withOidcFederationStrategyand optionally binds encryption to JWT claims./integrations/supabase/edge-functions— runs encryption in Deno through thewasm-inlineentry and caches CipherStash tokens across invocations./reference/stack/supabase— canonical wrapper reference: connect-time introspection, domain capabilities, the complete builder surface, response/error shapes, and deferred execution.Release alignment
encryptedSupabaseas the primary EQL v3 factory.encryptedSupabaseV3remains documented as a deprecated, type-identical alias.DATABASE_URLwhile application queries continue through PostgREST and preserve the wrapped client's Auth/RLS context.OidcFederationStrategy.create(), plain-object lock contexts, unsupported encrypted ordering domains, and.csv().Related integration work
Validation
bun run lintbun run validate-contentbun run validate-linksbun run validate-mermaidgit diff --check