diff --git a/.changeset/supabase-docs-wasm-inline-entry.md b/.changeset/supabase-docs-wasm-inline-entry.md new file mode 100644 index 000000000..89d409733 --- /dev/null +++ b/.changeset/supabase-docs-wasm-inline-entry.md @@ -0,0 +1,45 @@ +--- +'@cipherstash/stack-supabase': patch +'stash': patch +--- + +Stop telling customers the Supabase wrapper cannot run in a Worker. + +`@cipherstash/stack-supabase` has shipped two entry points since #912. The +package root introspects your database and runs on Node; the `wasm-inline` +entry carries the WASM engine, takes declared `schemas` instead of +introspecting, and runs on Deno, Supabase Edge Functions and Cloudflare +Workers. Introspection was the only thing that needed a Postgres socket, and +that entry does not do it. + +Two shipping documents were never updated and still described the state before +that change: + +- `packages/stack-supabase/README.md` said "the factory cannot run in an edge + Worker or the browser" and did not mention the `wasm-inline` entry anywhere + in the file. This is the npm package page. +- `skills/stash-supabase/SKILL.md` said the same thing in its setup section. + The skill ships inside the `stash` tarball, and `stash init` copies it into + the customer's own repository, where their coding agent reads it as + instruction. The one correct mention of the edge entry was in a callout near + the top that the setup steps never pointed at, so a reader following the + setup never learned the second entry existed. + +The population this misled hardest is the one that needs the edge entry most: +server code on Lovable, v0, Bolt and Replit runs on an edge runtime, which is +exactly the case `wasm-inline` was built for and exactly the case these +documents called impossible. + +Both files now describe both entries. The README gains an "Edge runtimes" +section with the call shape; the skill gains a fifth setup step with the same, +and the introspection paragraph now scopes its restriction to the native entry +and points there. Both name the four ways the edge entry differs: `schemas` is +required, `config` is required and must carry all four `CS_*` values, +`databaseUrl` is refused, and `.withLockContext()` / `.audit()` throw rather +than silently dropping an identity claim (#797). + +The **browser** half of the old sentence was correct and is kept, with the +reason now given: the WASM client requires a workspace `clientKey` on every +authentication path, so a browser build would ship the key with it (#804). + +Documentation only — no runtime behaviour changes. diff --git a/packages/stack-supabase/README.md b/packages/stack-supabase/README.md index f3a980ed8..ef973abb9 100644 --- a/packages/stack-supabase/README.md +++ b/packages/stack-supabase/README.md @@ -88,8 +88,8 @@ config to maintain — `select('*')` just works, inserts and updates encrypt aut reads decrypt automatically. Introspection needs a direct Postgres connection (`DATABASE_URL`), so `pg` is an optional peer -dependency and the factory cannot run in an edge Worker or the browser — construct it in your -server-side code. +dependency and this entry runs on Node only — construct it in your server-side code. For an +edge runtime, see the second entry point below. It runs alongside Supabase Auth and RLS, and supports [identity-locking encryption][identity] — binding a row's data key to the signed-in user's @@ -98,6 +98,47 @@ JWT claim — via the same lock-context API as the rest of the Stack. > `encryptedSupabaseV3` remains as a `@deprecated`, type-identical alias of `encryptedSupabase`, > so existing imports keep working. +## Edge runtimes: `@cipherstash/stack-supabase/wasm-inline` + +Deno, Supabase Edge Functions and Cloudflare Workers cannot load a native module or open a raw +Postgres socket. The `wasm-inline` entry point has neither requirement: the encryption engine is +a WASM blob inlined into the bundle, and you declare your tables instead of introspecting them. + +| Entry point | Engine | Schema | Runs on | +| --- | --- | --- | --- | +| `@cipherstash/stack-supabase` | native | introspected from the `public.eql_v3_*` domains | Node | +| `@cipherstash/stack-supabase/wasm-inline` | WASM, inlined | declared — `schemas` is required | Deno, Supabase Edge Functions, Cloudflare Workers | + +Everything after construction is the same wrapper — `from()`, the filters, and the response +shape are identical. + +```ts +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { encryptedSupabase } from '@cipherstash/stack-supabase/wasm-inline' + +const users = encryptedTable('users', { email: types.TextSearch('email') }) + +const es = await encryptedSupabase(supabaseUrl, supabaseKey, { + schemas: { users }, + config: { + workspaceCrn: Deno.env.get('CS_WORKSPACE_CRN')!, + accessKey: Deno.env.get('CS_CLIENT_ACCESS_KEY')!, + clientId: Deno.env.get('CS_CLIENT_ID')!, + clientKey: Deno.env.get('CS_CLIENT_KEY')!, + }, +}) +``` + +Four differences from the entry above, three of them enforced by the type checker: `schemas` is +required, because nothing introspects here; `config` is required and must carry all four `CS_*` +values, because there is no `~/.cipherstash` on an edge runtime; `databaseUrl` is refused; and +`.withLockContext()` / `.audit()` throw rather than silently dropping the identity claim — the +WASM engine does not implement them yet ([#797][issue-797]). + +This entry is ESM-only, and it is server-side rather than browser-safe: the WASM client requires +a workspace `clientKey` on every authentication path, so a browser build would ship the key with +it ([#804][issue-804]). + ## How it works
@@ -132,3 +173,5 @@ it should, and the EQL install needs no superuser (it works on cloud-hosted Supa
[eql]: https://github.com/cipherstash/encrypt-query-language
[stack-drizzle]: https://www.npmjs.com/package/@cipherstash/stack-drizzle
[stack-prisma]: https://www.npmjs.com/package/@cipherstash/stack-prisma
+[issue-797]: https://github.com/cipherstash/stack/issues/797
+[issue-804]: https://github.com/cipherstash/stack/issues/804
diff --git a/skills/stash-supabase/SKILL.md b/skills/stash-supabase/SKILL.md
index 442fba453..f7e9c9be9 100644
--- a/skills/stash-supabase/SKILL.md
+++ b/skills/stash-supabase/SKILL.md
@@ -26,7 +26,7 @@ selects, with support for equality, range, and ordering.
- Using identity-aware encryption (lock contexts) with Supabase
- Building applications where sensitive columns need encryption at rest and in transit
-> **On a managed AI platform — Lovable, v0, Bolt, Replit — read `stash-managed-platforms` first.** Two things there are decided before anything on this page applies: server code runs on an edge runtime, so it needs `@cipherstash/stack/wasm-inline` (`@cipherstash/protect` is the deprecated predecessor and its native module will not load — that dead end has cost an agent a whole turn), and the database role is not `postgres`, which changes how EQL gets installed. `encryptedSupabase` can be constructed inside a Worker, but only from the `@cipherstash/stack-supabase/wasm-inline` entry and only with declared `schemas` — introspection is what needs a Postgres connection, and declaring your tables is what skips it.
+> **On a managed AI platform — Lovable, v0, Bolt, Replit — read `stash-managed-platforms` first.** Two things there are decided before anything on this page applies: server code runs on an edge runtime, so it needs `@cipherstash/stack/wasm-inline` (`@cipherstash/protect` is the deprecated predecessor and its native module will not load — that dead end has cost an agent a whole turn), and the database role is not `postgres`, which changes how EQL gets installed. `encryptedSupabase` can be constructed inside a Worker, but only from the `@cipherstash/stack-supabase/wasm-inline` entry and only with declared `schemas` — introspection is what needs a Postgres connection, and declaring your tables is what skips it. Step 5 of Setup has the call shape.
> **What survives PostgREST, in one line** (the full treatment is under [Query behaviour on encrypted columns](#query-behaviour-on-encrypted-columns), a long way down): `eq` / `neq` / `in` / `match()` and the range filters `gt` / `gte` / `lt` / `lte` **do** work on capable domains, and so does `order()` on OPE-backed ordering columns. Encrypted free-text `matches()` and encrypted-JSON `contains()` / `selectorEq()` / `selectorNe()` **do not** — they need `eql_v3.query_*` casts PostgREST cannot emit, and the wrapper fails fast rather than returning wrong rows. Agents guess wrong in both directions on this, so don't infer it; for the predicates that don't survive, use Drizzle, Prisma Next, or SQL in an RPC.
@@ -67,10 +67,12 @@ this is also how **Supabase Edge Functions** get credentials in local dev —
> needs only a grant — so a reader granted the writer's keyset but bound to
> a different one decrypts fine while its searches silently return zero
> rows. `stash-zerokms` is canonical for keyset scoping, `stash-auth` for
-> credentials. Encryption *inside* an
-> Edge Function (Deno, no native modules) uses the
-> `@cipherstash/stack/wasm-inline` entry — see the `stash-edge` skill; SQL
-> written by hand in a migration or RPC is covered by `stash-postgres`.
+> credentials. Inside an
+> Edge Function (Deno, no native modules) the wrapper comes from
+> `@cipherstash/stack-supabase/wasm-inline` — step 5 below. For encryption
+> without the Supabase wrapper it is `@cipherstash/stack/wasm-inline`, see the
+> `stash-edge` skill; SQL written by hand in a migration or RPC is covered by
+> `stash-postgres`.
### 1. Install EQL v3 on the database
@@ -266,7 +268,9 @@ detects EQL v3 columns by their Postgres domain, derives each column's
encryption config from the domain, and builds the encryption client
internally — there is no client-side schema to hand-maintain. Introspection
needs a direct Postgres connection (`options.databaseUrl`, defaulting to
-`DATABASE_URL`), so the factory cannot run in a Worker or the browser.
+`DATABASE_URL`), and the engine is a native module, so **this entry runs on
+Node only**. On an edge runtime, import the `wasm-inline` entry instead —
+step 5 below.
Options: `{ schemas?, databaseUrl?, config? }` — `config` is the encryption
client config (e.g. `config.authStrategy`, see Authentication below).
@@ -310,6 +314,65 @@ A JS property may map to a different DB column name
are translated automatically, and `date`/`timestamp` columns decrypt to real
`Date` objects.
+### 5. Edge runtimes — the `wasm-inline` entry
+
+Deno, Supabase Edge Functions and Cloudflare Workers cannot load a native
+module and cannot open a raw Postgres socket. Both of those are properties of
+the entry above, not of the wrapper, so the package ships a second entry that
+has neither:
+
+| Entry | Engine | Schema | Runs on |
+|---|---|---|---|
+| `@cipherstash/stack-supabase` | native | introspected | Node |
+| `@cipherstash/stack-supabase/wasm-inline` | WASM, inlined into the bundle | declared — `schemas` is required | Deno, Supabase Edge Functions, Cloudflare Workers |
+
+Everything after construction is the same wrapper: `from()`, the filters, the
+transforms, and the response shape are identical.
+
+```typescript
+import { encryptedTable, types } from "@cipherstash/stack/eql/v3"
+import { encryptedSupabase } from "@cipherstash/stack-supabase/wasm-inline"
+
+const users = encryptedTable("users", {
+ email: types.TextSearch("email"),
+ amount: types.IntegerOrd("amount"),
+})
+
+const es = await encryptedSupabase(supabaseUrl, supabaseKey, {
+ schemas: { users },
+ config: {
+ workspaceCrn: Deno.env.get("CS_WORKSPACE_CRN")!,
+ accessKey: Deno.env.get("CS_CLIENT_ACCESS_KEY")!,
+ clientId: Deno.env.get("CS_CLIENT_ID")!,
+ clientKey: Deno.env.get("CS_CLIENT_KEY")!,
+ },
+})
+
+await es.from("users").select("id, email").eq("email", "a@b.com")
+```
+
+Four differences from the native entry, three of them enforced by the type
+checker:
+
+- **`schemas` is required.** Nothing introspects here, so a client built
+ without a declaration has no columns and encrypts nothing. Declare every
+ table you touch — undeclared tables pass through unencrypted.
+- **`config` is required, and must carry all four `CS_*` values.** There is no
+ `~/.cipherstash` on an edge runtime to discover credentials from. Mint them
+ with `stash env --name