docs(supabase): document the wasm-inline entry in the two files that ship - #951
Conversation
🦋 Changeset detectedLatest commit: 4d71746 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Review of #951 found four wrong statements in files that ship to customers. Verified each against source before fixing; one was misattributed and the real defect turned out to be older and elsewhere. - `skills/stash-managed-platforms/SKILL.md` shipped a snippet that does not compile: it authored `schemas` from `@cipherstash/stack/wasm-inline` and handed them to `encryptedSupabase`, whose `schemas` is typed from `@cipherstash/stack/eql/v3`. The two entries ship independent declarations of the column classes, whose private `columnName` field TypeScript compares nominally, so `tsc` rejects it while the code runs fine. Nothing caught it — `column-map.ts` probes structurally by design, and `e2e/wasm/deno.json` runs `deno test --no-check`. - `skills/stash-edge/SKILL.md` is what produced that snippet: it told edge projects to author schemas from the WASM entry with no carve-out. The rule is really "author against the entry whose client type consumes the schema". - `skills/stash-supabase/SKILL.md` named the wrong hazard. An undeclared table throws and a missing `schemas` cannot construct; what is silently treated as plaintext is an undeclared *column* on a declared table. - "Undeclared tables behave exactly as with no `schemas` at all" was false on the native entry too — introspection is gated on a resolved database URL, not on the absence of `schemas`. - "config must carry all four `CS_*` values" was wrong in five places. `WasmClientConfig` is a union: only `clientId` and `clientKey` are always required, and the `authStrategy` arm makes `workspaceCrn` optional. OIDC federation does work on the edge; `.withLockContext()` is what does not. `stash-edge` already contradicted itself here — its own example passes two. Three tests keep these honest, each confirmed to fail when its property is violated: - `scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs` fails if any shipped document pairs the adapter's edge entry with a WASM-entry schema. - `supabase-wasm-config.test-d.ts` pins the config union at the type level. - `supabase-declared-mode.test.ts` pins the undeclared-column plaintext path across insert, update, filter, select and decrypt. Documentation and tests only — no runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01UqnsDeKs8N9n9TStPAqzzw
The reference doc and three TSDoc blocks all derived the default entry's
runtime from schema discovery: introspection needs Postgres, therefore the
entry cannot run on an edge runtime. That inference is false in both
directions. Declaring `schemas` removes the Postgres dependency entirely
(create.ts:303-306, :364-367) and the entry is still Node-only; and the
entry would be Node-only with no introspection code in it at all.
What actually pins it is the import: `Encryption` from `@cipherstash/stack`
pulls a module graph that statically imports `@cipherstash/auth`, whose Node
entry resolves its platform binding at module evaluation, and the emitted
bundle carries an `import("pg")` specifier a bundler resolves at build time.
Neither moves when you declare schemas.
The default entry's doc also named `@cipherstash/protect-ffi` as the binary
loaded on import. It is the one package in that graph that deliberately does
not: `packages/protect-ffi/src/index.cts` uses `import native = require(...)`
specifically so `__importStar` cannot force the neon proxy to resolve, and
`nativeLoading.test.ts` guards it.
Two smaller corrections in the same pass: bare "a Worker" is ambiguous and
false under the Node `worker_threads` reading — the native entry runs fine
there — so the edge runtimes are now named, as the table already named them;
and the browser prohibition is restored to the native entry, which the
previous revision moved onto the edge entry, leaving the native paragraph
implying the browser was fine.
Guarded by scripts/__tests__/supabase-runtime-claims.test.mjs (three
detectors, unit-tested in both directions, applied to the four prose
sources), and by three new assertions in wasm-entry-edge-safety.test.ts that
tie the corrected prose to the emitted bundles — its header comment repeated
the protect-ffi misattribution and would otherwise have contradicted them.
Not touched, to avoid conflicting with open PRs: skills/stash-supabase and
packages/stack-supabase/README.md carry defect 1 verbatim but are being
rewritten on #951 at those exact lines, and the browser-capability claims in
examples/ and packages/stack/tsup.config.ts belong to #953. The README path
is recorded in the guard's GUARDED list comment so it is added when #951
lands.
Claude-Session: https://claude.ai/code/session_01FVKXa6GjUHN5xvJq2912KA
|
Flagging one sentence here from #952, which corrects this same explanation elsewhere.
The The reason to raise it now rather than after merge: #952 adds a prose guard ( Worth noting the neighbouring sentence in
That names the engine as a cause, which is the fix. The README just needs the same treatment. Something like:
That also restores the browser half, which the current rewrite drops. |
freshtonic
left a comment
There was a problem hiding this comment.
Requesting changes for one narrowly-scoped reason; everything else in this PR verified cleanly and is approve-quality. Details below.
The blocker: the README's corrected sentence still carries the false causal claim
packages/stack-supabase/README.md (~line 90) now reads:
Introspection needs a direct Postgres connection (
DATABASE_URL), sopgis an optional peer dependency and this entry runs on Node only
The "so" still derives runs on Node only from introspection — the precise causal inversion #952 exists to correct, and which its changeset corrects in this same package's .d.ts hover text. The two shipping surfaces would disagree about causality after both merge. The harm case is the one #952 documents: a reader who believes the causal version passes schemas (which really does remove introspection) expecting an edge-capable client, and gets a build failure — the entry is Node-only because it binds the native engine, schemas or not.
This is also pre-announced churn: #952's GUARDED comment defers adding this README to the falseRuntimeCause guard until after this PR lands, at which point the guard will flag this exact sentence and force a third PR to rewrite it. Cheaper to land it right here. Suggested wording:
Introspection needs a direct Postgres connection (
DATABASE_URL), which is whypgis an optional peer dependency. This entry runs on Node only either way — it binds the native engine, and declaringschemasdoesn't move that — so construct it in your server-side code. For an edge runtime, see the second entry point below.
Two smaller instances of the same shape, recommended but not blocking:
skills/stash-supabase/SKILL.mdstep 3: "Introspection needs a direct Postgres connection (…), and the engine is a native module, so this entry runs on Node only". This one at least names the true cause, but still lists introspection as co-cause of the runtime restriction. Lead with the engine, keep introspection as the reason for the connection, not the runtime.- The managed-platforms callout's "
encryptedSupabasecan be constructed inside a Worker" sentence has no edge-runtime qualifier in the sentence itself — the readingworker_threadsmakes it ambiguous per #952's third defect. One word ("edge Worker" → name the runtimes) fixes it.
What I verified (all checks out)
- The
WasmClientConfigunion claims are exactly right.packages/stack/src/wasm-inline.ts:221-270:clientId/clientKeyon the base; the access-key arm requiresworkspaceCrn+accessKeywithauthStrategy?: never; the strategy arm makesworkspaceCrnoptional withaccessKey: never. The README/skill prose, the type test's positive and negative cases, and the "mixing rejected" case all match the type as written. - The
eql/v3-vs-wasm-inlineschema-entry correction is real.schema-builder.ts:3,7typesschemasasRecord<string, AnyV3Table>from@cipherstash/stack/eql/v3;column-map.tsdocuments the structural (non-instanceof) probe that let the broken snippet run at runtime;e2e/wasm/deno.jsonruns--no-check, confirming nothing in-repo would have caught the shipped snippet. The managed-platforms skill really did ship a snippet that failstsc. - The declared-mode hazard rewrite matches the code.
create.ts:285-304: the ambientDATABASE_URLread is gated onintrospector, so "undeclared tables behave exactly as with noschemas" was indeed false withoutdatabaseUrlpassed alongside. Theselect('*')refusal (query-builder.ts:162) and the raw-*no-.select()branch (:724) are both as the new skill text describes — including the important point that the refusal is not a read backstop. - The new wire-level tests pin the real hazard (undeclared column: plaintext on insert/update/filter, no
::jsonbcast, absent from the decrypt table) at the observable-payload level, and the type test carries a positive control so the@ts-expect-errors can't go vacuous. - The new guard runs and can't go silently empty:
lib/package-readmes.mjsexists on main, the shipped-file set is asserted non-trivial, and per-block (not per-file) matching correctly permits the raw-client snippet and the adapter snippet in the same document. - Changeset scope follows repo precedent: pending skills changesets (
supabase-skill-eql-305,skills-eql-source-and-index-cascade,prisma-skill-upgrade-replan) all bumpstashonly, sostash+@cipherstash/stack-supabaseis right here. (Wizard also shipsskills/in its tarball and conventionally gets no bump — if that convention is wrong it's wrong repo-wide, not in this PR.) - CI is green; the #953 conflict note is accurate and the merge-second-rebases plan is fine.
Fix the README sentence (and ideally the two skill instances) and this is an approve — the rest of the PR is careful, verified work, and the four corrected claims in the changeset are each genuinely load-bearing for edge users.
ReviewThe correction this PR makes is right and overdue, and the discipline in commit 3 — noticing that the Medium1. Concretely: someone ports the Quick-start snippet into a Supabase Edge Function exactly as the new section instructs, and gets 2.
That is precisely the inference commit 3 identified as wrong. An agent on Lovable writes Low3. This is the failure mode the repo already defends against one file over — 4. 5. HousekeepingThe PR body describes only the first commit. "Changes" lists three files; the diff is nine. Commits Verified, not findings
|
coderdan
left a comment
There was a problem hiding this comment.
Requesting changes on one item. Full findings are in #951 (comment) — this review just marks which of them block.
Blocking — skills/stash-managed-platforms/SKILL.md:181-183. Commit 3 of this PR established that the select('*') refusal is not a read backstop: a query awaited with no .select() call takes the raw-* branch (query-builder.ts:720-724) and decryptResults passes it through on !hasSelect (query-results.ts:127-130), returning every column undecrypted. That correction landed in stash-supabase/SKILL.md and not in this file, which the same PR edits. It still reads "What declared mode gives up, it gives up loudly rather than silently" over a bullet saying select('*') and bare select() are refused.
This skill ships into customer repositories and is read as instruction by an agent on Lovable, v0, Bolt or Replit. The failure it mispromises is silent — raw EQL payloads returned as data, no error. Shipping the correction to one of two copies is worse than shipping neither, because the copy left behind now looks reviewed. Carrying commit 3's wording across is the whole fix.
Worth fixing in the same pass, not blocking — packages/stack-supabase/README.md:112. "Everything after construction is the same wrapper" contradicts README:87 twenty-five lines above, and the edge entry refuses select('*') in exactly the Quick-start snippet the new section tells readers to port. One sentence, and it renders on the npm package page.
Findings 3, 4 and 5 in the comment are fine as follow-ups. Finding 3 has a real one-line fix (databaseUrl?: never) if you would rather the "three of them enforced by the type checker" claim become true than be softened.
The central correction here is right and the new guard test is real — this is a short round trip, not a rework. Please also refresh the PR body: it describes only the first of three commits, and the "Verification" runs predate the ~440 lines of tests added since.
…ead-backstop correction across Review of #951 found four things. One is a change to a published type; three are corrections to shipped text, two of them in the copy the previous commit did not touch. `EncryptedSupabaseWasmOptions` enforced less than three documents claimed it did. Leaving `databaseUrl` off the interface is policed by excess-property checking, which fires on FRESH object literals alone — so an options object assembled as a `const` and passed by variable, which is what a Node-to-edge port actually holds, type-checked clean and reached the construction-time throw in `create.ts` instead. The README, `stash-supabase` and the interface's own doc comment all said the type checker enforced it. The field is now declared `databaseUrl?: never`, which is the same gap and the same fix as `WasmClientConfig.eqlVersion?: never` one package along, whose comment describes this exact shared-config-const path. The runtime throw stays: it is the only thing a plain JS caller meets. New type tests cover both the inline and the by-variable shapes on both call forms, and a positive control asserts the same options object still compiles once the field is dropped — without it, a `never` that poisoned the options type would read as four passing expect-errors. `578783ad` corrected the `select('*')` claim in `stash-supabase` and left the same claim standing in `stash-managed-platforms`, which the same PR edited. That skill still framed declared mode as giving things up "loudly rather than silently" over a bullet naming the refusal — precisely the inference the correction exists to kill. Its audience is an agent on Lovable, v0, Bolt or Replit reading it as instruction, and the failure it mispromised is silent: `await supabase.from('users').eq('id', 1)` with no `.select()` takes the raw-`*` branch (`query-builder.ts:703-725`), `decryptResults` returns it untouched on `!hasSelect` (`query-results.ts:126-138`), and every column comes back as a raw EQL payload with no error. Shipping a correction to one of two copies is worse than shipping neither, because the copy left behind now looks reviewed — so `scripts/__tests__/skills-select-star-not-a-read-backstop.test.mjs` now fails if any shipped document states the refusal without the caveat in the same markdown section. It scans per section rather than per sentence, so the wording can move; it blanks fenced code, so a snippet is not read as a claim; and it carries a liveness test against the canonical skill, because a decayed regex is a guard that always passes. "Everything after construction is the same wrapper" was false in the first way a reader hits it, in both the README and `stash-supabase`. The edge entry is always in declared mode, where `select('*')` is refused and `from()` on an undeclared table throws — and the README said it twenty-five lines under a paragraph telling the same reader `select('*')` just works, immediately above the quick-start snippet it tells you to port into an Edge Function. Both sentences now name the two exceptions. `stash-managed-platforms` also promised a diagnostic that cannot fire on the entry its section is about: an ambient `DATABASE_URL` ignored "with a warning that the declaration is unverified". Both the ambient read (`create.ts:304`) and the warning (`create.ts:330`) are gated on `introspector`, which is `null` for the wasm-inline build. Scoped to Node, with the edge case stated. This is the same correction `stash-supabase` already carried and this file did not. The changeset gains `@cipherstash/wizard`: `skills/` ships in that tarball too (`packages/wizard/tsup.config.ts` copies it to `dist/skills`, listed under `files`), so without the bump the published wizard keeps the old text until an unrelated change moves its version. The recent skills-editing changesets were `stash`-only; the skills-adding ones bumped both. The ambient-warning claim is deliberately left unguarded — "a warning is logged" has no stable phrasing to key on and "introspector" appears freely in any section about introspection, so every detector for it either misses rewordings or fires on unrelated prose. The header comment says so. Verification: `@cipherstash/stack-supabase` 570 tests + 56 type tests, 0 type errors; `pnpm run test:scripts` 887 passed (51 files), including both doc guards; `skill-supabase-apply` 29 passed; `biome check` 0 errors. The new guard was confirmed red against the unmodified `stash-managed-platforms` and against a README with the caveat clause removed, before either was fixed. Claude-Session: https://claude.ai/code/session_01TvoE76yuuLn2Bmdaz8yFYy
freshtonic
left a comment
There was a problem hiding this comment.
Re-reviewed at becdebb. Still requesting changes, for the same single item as my last review — it is the one thing the new commit did not touch.
Still blocking — packages/stack-supabase/README.md (~line 90):
Introspection needs a direct Postgres connection (
DATABASE_URL), sopgis an optional peer dependency and this entry runs on Node only
The "so" still derives runs on Node only from introspection — the causal inversion #952 (now merged) corrects in this same package's .d.ts, and the sentence #952's falseRuntimeCause guard will mechanically flag the moment this README is added to its GUARDED list, which this PR's own comments schedule. One clause fixes it; suggested wording from my previous review still applies:
Introspection needs a direct Postgres connection (
DATABASE_URL), which is whypgis an optional peer dependency. This entry runs on Node only either way — it binds the native engine, and declaringschemasdoesn't move that — so construct it in your server-side code. For an edge runtime, see the second entry point below.
The two smaller instances I flagged as non-blocking are also still present (skills/stash-supabase/SKILL.md:271's "and the engine is a native module, so…" co-cause form, and the line-29 bare "Worker") — still non-blocking, still worth the one-word fixes while in the file.
The new commit itself is good, verified:
databaseUrl?: neveronEncryptedSupabaseWasmOptionsis a real fix, and the rationale is exactly right: omission is policed by excess-property checking, which fires on fresh literals only, so the ported-from-Nodeconstoptions object type-checked clean and hit the runtime throw. The new type-test block pins the variable-passing case — the one that actually bites.- Dan's blocker (the read-backstop correction missing from
stash-managed-platforms) is carried across, and the newskills-select-star-not-a-read-backstop.test.mjsguard means the "gives up loudly" phrasing can't drift back into any shipped document. - The
@cipherstash/wizard: patchaddition to the changeset is correct and goes further than repo precedent required — wizard shipsskills/in its own tarball, so this is the more honest bump set. - CI is green at HEAD.
Fix the one README sentence and I'll approve on sight.
…ship `@cipherstash/stack-supabase` has had two entry points since #912. The package root introspects the database and runs on Node; `/wasm-inline` 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 needing a Postgres socket, so that entry does run in a Worker. Two shipping documents were never updated and still describe the state before that change: - `packages/stack-supabase/README.md` said the factory "cannot run in an edge Worker or the browser", and contained no occurrence of the word "wasm" at all. This file renders on the npm package page. - `skills/stash-supabase/SKILL.md` said the same in its setup section. The skill ships inside the `stash` tarball and `stash init` copies it into the customer's repo, where their agent reads it as instruction. The file's one correct mention of the edge entry sat in a callout the setup steps never pointed at, so a reader following the steps never learned it existed. The readers this misled hardest are the ones who need the edge entry most: server code on Lovable, v0, Bolt and Replit runs on an edge runtime, which is exactly what `/wasm-inline` was built for. The failure is silent — an agent that reads "cannot run in a Worker" concludes the product does not support the platform and stops. Both files now carry an entry-point table and the edge call shape, and name the four ways that entry differs, each checked against the source rather than restated: `schemas` is required (`create.ts:307-313`), `config` is required and carries all four `CS_*` values (`wasm-inline.ts:18-30`), `databaseUrl` throws at construction (`create.ts:359-361`), and `.withLockContext()` / `.audit()` throw rather than silently dropping an identity claim (`wasm-client-adapter.ts:40-66`, #797). The browser half of the old sentence was correct and is kept, with its reason named: the WASM client requires a workspace `clientKey` on every authentication path, so a browser build would ship the key with it (#804). Deliberately untouched: `packages/stack/CHANGELOG.md`, `packages/stack-supabase/CHANGELOG.md` and the superpowers design spec carry the same sentence and are historical records, accurate for their dates. `docs/reference/supabase-sdk.md` carries it too and is fixed separately — it is internal and ships in no package. Changeset: `stash` patch for the skill, `@cipherstash/stack-supabase` patch for the README. Closes #950 Refs #912, #804, #797 Claude-Session: https://claude.ai/code/session_01E1J2nVGJWVkqvLepDfinRf
Review of #951 found four wrong statements in files that ship to customers. Verified each against source before fixing; one was misattributed and the real defect turned out to be older and elsewhere. - `skills/stash-managed-platforms/SKILL.md` shipped a snippet that does not compile: it authored `schemas` from `@cipherstash/stack/wasm-inline` and handed them to `encryptedSupabase`, whose `schemas` is typed from `@cipherstash/stack/eql/v3`. The two entries ship independent declarations of the column classes, whose private `columnName` field TypeScript compares nominally, so `tsc` rejects it while the code runs fine. Nothing caught it — `column-map.ts` probes structurally by design, and `e2e/wasm/deno.json` runs `deno test --no-check`. - `skills/stash-edge/SKILL.md` is what produced that snippet: it told edge projects to author schemas from the WASM entry with no carve-out. The rule is really "author against the entry whose client type consumes the schema". - `skills/stash-supabase/SKILL.md` named the wrong hazard. An undeclared table throws and a missing `schemas` cannot construct; what is silently treated as plaintext is an undeclared *column* on a declared table. - "Undeclared tables behave exactly as with no `schemas` at all" was false on the native entry too — introspection is gated on a resolved database URL, not on the absence of `schemas`. - "config must carry all four `CS_*` values" was wrong in five places. `WasmClientConfig` is a union: only `clientId` and `clientKey` are always required, and the `authStrategy` arm makes `workspaceCrn` optional. OIDC federation does work on the edge; `.withLockContext()` is what does not. `stash-edge` already contradicted itself here — its own example passes two. Three tests keep these honest, each confirmed to fail when its property is violated: - `scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs` fails if any shipped document pairs the adapter's edge entry with a WASM-entry schema. - `supabase-wasm-config.test-d.ts` pins the config union at the type level. - `supabase-declared-mode.test.ts` pins the undeclared-column plaintext path across insert, update, filter, select and decrypt. Documentation and tests only — no runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01UqnsDeKs8N9n9TStPAqzzw
Adversarial verification of the previous commit found four problems in its own
corrections. Three are fixed here; the fourth is pre-existing and noted below.
The worst was a mitigation that does not hold. The bullet claimed `select('*')`
being refused in declared mode means "ciphertext only comes back if you name
the column yourself". The refusal is real and unconditional, but it guards the
wrong door: a read awaited with no `.select()` call at all takes the raw-`*`
branch in `query-builder.ts` and `decryptResults` passes it through on
`!hasSelect`, so every column returns undecrypted — declared or not.
`query-builder.ts:703-720` documents this as long-standing behaviour
deliberately left alone; the docs just did not reflect it. So writes have a
backstop (the domain CHECK, which a NULL still passes) and reads have none.
Corrected in the skill, in the new test's header comment, and in the changeset.
Also: `WasmClientConfig` has three arms, not two — the third is the deprecated
`strategy` alias. Naming it in user docs would advertise what we are removing,
so the fix is to stop counting rather than to document it. And `stash-edge`'s
new exception section said the failure came "from the other direction", which
contradicts the section above it that already establishes assignment is
rejected both ways; it is the same rejection reported one level up, at
`AnyV3Table` rather than at the column.
Not fixed here, worth a follow-up: `e2e/wasm/supabase-declared.test.ts` pairs
`encryptedSupabase` with a schema from `@cipherstash/stack/wasm-inline` — the
exact pairing this PR documents as non-compiling — hidden by `deno test
--no-check`. It is a genuine counterexample, but it is an internal test that
ships to nobody, and at lines 255-284 one table object feeds both a raw
wasm-inline `Encryption` and the adapter, so the fix needs two schema objects
rather than a swapped import. The new guard cannot catch it either: its
extractor reads fenced markdown blocks, so covering `.ts` files is a different
test with its own scope question.
Claude-Session: https://claude.ai/code/session_01UqnsDeKs8N9n9TStPAqzzw
…ead-backstop correction across Review of #951 found four things. One is a change to a published type; three are corrections to shipped text, two of them in the copy the previous commit did not touch. `EncryptedSupabaseWasmOptions` enforced less than three documents claimed it did. Leaving `databaseUrl` off the interface is policed by excess-property checking, which fires on FRESH object literals alone — so an options object assembled as a `const` and passed by variable, which is what a Node-to-edge port actually holds, type-checked clean and reached the construction-time throw in `create.ts` instead. The README, `stash-supabase` and the interface's own doc comment all said the type checker enforced it. The field is now declared `databaseUrl?: never`, which is the same gap and the same fix as `WasmClientConfig.eqlVersion?: never` one package along, whose comment describes this exact shared-config-const path. The runtime throw stays: it is the only thing a plain JS caller meets. New type tests cover both the inline and the by-variable shapes on both call forms, and a positive control asserts the same options object still compiles once the field is dropped — without it, a `never` that poisoned the options type would read as four passing expect-errors. `578783ad` corrected the `select('*')` claim in `stash-supabase` and left the same claim standing in `stash-managed-platforms`, which the same PR edited. That skill still framed declared mode as giving things up "loudly rather than silently" over a bullet naming the refusal — precisely the inference the correction exists to kill. Its audience is an agent on Lovable, v0, Bolt or Replit reading it as instruction, and the failure it mispromised is silent: `await supabase.from('users').eq('id', 1)` with no `.select()` takes the raw-`*` branch (`query-builder.ts:703-725`), `decryptResults` returns it untouched on `!hasSelect` (`query-results.ts:126-138`), and every column comes back as a raw EQL payload with no error. Shipping a correction to one of two copies is worse than shipping neither, because the copy left behind now looks reviewed — so `scripts/__tests__/skills-select-star-not-a-read-backstop.test.mjs` now fails if any shipped document states the refusal without the caveat in the same markdown section. It scans per section rather than per sentence, so the wording can move; it blanks fenced code, so a snippet is not read as a claim; and it carries a liveness test against the canonical skill, because a decayed regex is a guard that always passes. "Everything after construction is the same wrapper" was false in the first way a reader hits it, in both the README and `stash-supabase`. The edge entry is always in declared mode, where `select('*')` is refused and `from()` on an undeclared table throws — and the README said it twenty-five lines under a paragraph telling the same reader `select('*')` just works, immediately above the quick-start snippet it tells you to port into an Edge Function. Both sentences now name the two exceptions. `stash-managed-platforms` also promised a diagnostic that cannot fire on the entry its section is about: an ambient `DATABASE_URL` ignored "with a warning that the declaration is unverified". Both the ambient read (`create.ts:304`) and the warning (`create.ts:330`) are gated on `introspector`, which is `null` for the wasm-inline build. Scoped to Node, with the edge case stated. This is the same correction `stash-supabase` already carried and this file did not. The changeset gains `@cipherstash/wizard`: `skills/` ships in that tarball too (`packages/wizard/tsup.config.ts` copies it to `dist/skills`, listed under `files`), so without the bump the published wizard keeps the old text until an unrelated change moves its version. The recent skills-editing changesets were `stash`-only; the skills-adding ones bumped both. The ambient-warning claim is deliberately left unguarded — "a warning is logged" has no stable phrasing to key on and "introspector" appears freely in any section about introspection, so every detector for it either misses rewordings or fires on unrelated prose. The header comment says so. Verification: `@cipherstash/stack-supabase` 570 tests + 56 type tests, 0 type errors; `pnpm run test:scripts` 887 passed (51 files), including both doc guards; `skill-supabase-apply` 29 passed; `biome check` 0 errors. The new guard was confirmed red against the unmodified `stash-managed-platforms` and against a README with the caveat clause removed, before either was fixed. Claude-Session: https://claude.ai/code/session_01TvoE76yuuLn2Bmdaz8yFYy
…t introspection The README and the Supabase skill both wrote "introspection needs a direct Postgres connection, so ... this entry runs on Node only". Introspection is not what makes the entry Node-only — it binds the native engine, and declaring schemas does not move that. The stated cause invites the conclusion that declaring tables makes the root entry edge-capable, which is the wrong turn the wasm-inline entry exists to prevent. This is the same causal inversion #952 corrected in the package's .d.ts. Two smaller instances in skills/stash-supabase/SKILL.md go with it: a bare "Worker" where the surrounding paragraph means any edge runtime, and the co-cause "and the engine is a native module, so ..." split into its own sentence so the engine carries the claim alone. Claude-Session: https://claude.ai/code/session_01133kxi8qXu6UJPiKJDwDt6
…e-claims guard supabase-runtime-claims.test.mjs shipped with its own instruction: add packages/stack-supabase/README.md when #951 lands, held off only because this branch was rewriting the same lines. Both sentences now attribute the Node-only restriction to the engine, so the README goes on the list, and so does skills/stash-supabase/SKILL.md — the two copies that reach a customer, one as the npm package page and one copied into their repo by stash init. Enrolling the skill needed one sentence changed: "Removing the pg dependency would unblock Workers, not browsers" left "Workers" unqualified, and its opener made introspection the thing keeping the entry out of a browser. Neither is true of the browser, which is blocked by the workspace clientKey (#804) regardless. stash-managed-platforms is left off, with the reason recorded in the docblock: its causal claims are correct, but it trips the unqualified-"Worker" detector in its frontmatter description, where a reword changes what the skill matches on. Claude-Session: https://claude.ai/code/session_01133kxi8qXu6UJPiKJDwDt6
becdebb to
1a7db49
Compare
freshtonic
left a comment
There was a problem hiding this comment.
Approving at 1a7db49 — the blocker from my last two reviews is resolved, and better than asked: the README sentence now attributes Node-only to the engine with introspection kept to what it actually explains (the pg peer dep), the skill's step-3 sentence carries the same correct causality, and rather than deferring guard enrolment to a follow-up, this commit adds both the README and the Supabase skill to the #952 runtime-claims guard now — so the false-cause phrasing is mechanically unwritable from here on. CI green. Nice work on this whole family of PRs.
…refuses stash-managed-platforms told readers to pass databaseUrl "so introspection fills the gaps" when they cannot guarantee their schemas are complete. That sentence sits under "## encryptedSupabase in a Worker", four lines above the sentence saying databaseUrl is refused outright on that entry. Same fault as the ambient-DATABASE_URL sentence corrected in 981f86a, in the adjacent paragraph, and it lands on the readers this skill is written for: an agent on Lovable, v0, Bolt or Replit has only the edge entry, so following the remedy gets a construction throw. It now says introspection is unavailable there, that the declaration is the only thing between a schema change and a silent plaintext write, and that the Node entry is where databaseUrl covers you. Claude-Session: https://claude.ai/code/session_01133kxi8qXu6UJPiKJDwDt6
freshtonic
left a comment
There was a problem hiding this comment.
Re-approved at 4d71746 — the follow-up commit removes another self-contradiction (stash-managed-platforms recommended passing databaseUrl under the Worker heading, four lines from the sentence saying the edge entry refuses it) and keeps the changeset in step. Consistent with everything already approved.
Summary
CipherStash Stack encrypts individual database columns.
@cipherstash/stack-supabaseis the Supabase integration, and since #912 it has shipped two entry points: the package root, which reads your column types straight out of Postgres and therefore needs Node; and@cipherstash/stack-supabase/wasm-inline, which takes the tables you declare instead and therefore runs on Deno, Supabase Edge Functions and Cloudflare Workers.The documents that ship to customers were never updated. They still said the wrapper cannot run in a Worker, which stopped being true nine days ago. This PR corrects them and documents the edge entry, which the README did not mention even once.
This matters most for the readers who need the edge entry: server code on managed AI platforms (Lovable, v0, Bolt, Replit) runs on an edge runtime. An agent that reads "cannot run in a Worker" does not file a bug — it concludes the product does not support the platform and stops.
Writing the correct version turned up a set of claims in the same neighbourhood that were also wrong, and a type that enforced less than the docs said it did. Those are fixed here too, and three new test files stop each one drifting back.
The six commits
Rebased onto
mainafter #953 merged; the hashes below are post-rebase.e36f1a06wasm-inlineentry inpackages/stack-supabase/README.mdandskills/stash-supabase/SKILL.mdd35c792f0abd7be8d35c792f's own corrections — theselect('*')refusal is not a read backstop88264b79databaseUrlin the type, and carry0abd7be8across to the copy it missed3979948b1a7db4924d717468Changes
skills/stash-supabase/SKILL.md— ships inside thestashand@cipherstash/wizardnpm packages;stash initcopies it into the customer's repository, where their coding agent reads it as instruction.wasm-inlineentry": an entry-point comparison table, the full call shape, and the four ways that entry differs.@cipherstash/stack/wasm-inline, which is the right entry for encrypting without the wrapper and the wrong one for using it.packages/stack-supabase/README.md— renders on the npm package page. Same correction to the introspection paragraph, plus a new "Edge runtimes" section with the same table and call shape. The file contained zero occurrences of the word "wasm" before this.skills/stash-edge/SKILL.mdandskills/stash-managed-platforms/SKILL.md— corrected where they were wrong about this surface; see below.packages/stack-supabase/src/wasm-inline.ts—databaseUrl?: neveronEncryptedSupabaseWasmOptions. The only non-documentation change in the PR..changeset/supabase-docs-wasm-inline-entry.md—@cipherstash/stack-supabase,stashand@cipherstash/wizard, all patch.Corrections beyond the headline
Each was checked against source rather than restated.
stash-managed-platformsshipped a snippet that does not compile. It authoredschemasfrom@cipherstash/stack/wasm-inlineand handed them toencryptedSupabase. The adapter typesschemasfrom@cipherstash/stack/eql/v3, and the two entries ship independent declarations of the column classes whose privatecolumnNameTypeScript compares nominally —tscrejects it while the code runs perfectly, which is why nobody noticed.stash-edgeis what produced that snippet. Its "Schema Modules Do Not Cross Entries" section had no carve-out for the adapter. The real rule is "author against the entry whose client type consumes the schema".schemason the edge entry cannot produce a no-column client; the hazard is an undeclared column on a declared table, treated as plaintext.select('*')being refused is not a read backstop. A query awaited with no.select()call at all takes the raw-*branch (query-builder.ts:703-725) anddecryptResultspasses it through on!hasSelect(query-results.ts:126-138), returning every column undecrypted — declared or not, no error. Writes have a backstop (theeql_v3_*domain CHECK, though a NULL passes); reads have none.databaseUrlwas only refused for callers who wrote the options inline. Leaving the field off the interface is policed by excess-property checking, which fires on fresh object literals alone — an options object built as aconstand passed by variable type-checked clean and hit the runtime throw instead, from documents claiming the type checker enforced it. NowdatabaseUrl?: never, the same gap and fix asWasmClientConfig.eqlVersion?: neverone package along.select('*')just works, immediately above the snippet it tells you to port into an Edge Function. Both it andstash-supabasenow name the two exceptions.stash-managed-platformspromised an ambientDATABASE_URLis ignored "with a warning that the declaration is unverified", in a section aboutwasm-inline. Both the ambient read (create.ts:304) and the warning (create.ts:330) are gated onintrospector, which isnullon that build.schemasreally does skip introspection, and the entry is Node-only anyway — it binds the native engine, and its emitted bundle carries animport("pg")specifier a bundler resolves at build time. A reader who takes the stated cause at face value reaches forschemasexpecting an edge-capable client. Both the README andstash-supabasenow attribute the restriction to the engine, keeping introspection as the reason for thepgpeer dependency, which is what it actually explains. This is the same defect fix(supabase): document the second entry point, and pin the runtime to the engine #952 corrects in this package's.d.ts.stash-managed-platforms'databaseUrlso introspection fills the gaps" — under## encryptedSupabase in a Worker, four lines above the sentence sayingdatabaseUrlis refused there. Same shape as 7, in the adjacent paragraph, and it lands on this skill's actual readers: an agent on Lovable, v0, Bolt or Replit has only the edge entry, so following the remedy gets a construction throw.New tests
Nothing type-checks a SKILL.md or a README, and these are shipped text — the drift lands in someone else's repository, not ours. Three files close that:
scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs— fails if any shipped document pairsencryptedSupabasewith a schema authored from@cipherstash/stack/wasm-inline. Per fenced block, not per file.scripts/__tests__/skills-select-star-not-a-read-backstop.test.mjs— fails if any shipped document states theselect('*')refusal without the caveat, in the same markdown section. This one exists because the correction in578783adlanded in one of two copies and the copy left behind then looked reviewed. It scans per section so wording can move, blanks fenced code so a snippet is not read as a claim, and carries a liveness test against the canonical skill — a decayed regex is a guard that always passes.packages/stack-supabase/__tests__/supabase-wasm-config.test-d.ts— type-level: the edgeconfigaccepts both auth arms and rejectsclientId+clientKeyalone;databaseUrlis rejected inline and by variable, on both call forms, with a positive control so aneverthat poisoned the options type cannot masquerade as passing expect-errors.packages/stack-supabase/__tests__/supabase-declared-mode.test.ts— pins the real hazard at runtime: an undeclared column on a declared table reaches PostgREST as plaintext on insert, update and filter, and is absent from the decrypt call.Both doc guards were confirmed red before the fixes: the new one against the unmodified
stash-managed-platforms, and again against a README with the caveat clause deleted.Correction 8 is now mechanically enforced too, in this PR rather than a follow-up. #952's
scripts/__tests__/supabase-runtime-claims.test.mjsshipped with a note in its ownGUARDEDlist deferringpackages/stack-supabase/README.mduntil this branch stopped rewriting those lines.1a7db492adds it, and addsskills/stash-supabase/SKILL.mdbeside it — the two copies that reach a customer, one as the npm package page and one copied into their repository bystash init. Enrolling the skill needed one more sentence fixed ("Removing thepgdependency would unblock Workers, not browsers" left "Workers" unqualified, and its opener made introspection the thing keeping the entry out of a browser; the browser is ruled out by the workspaceclientKey, #804, either way).skills/stash-managed-platforms/SKILL.mdis deliberately not enrolled, with the reason recorded in the docblock: its causal claims are correct, but it trips the unqualified-"Worker" detector twice inside its YAML frontmatterdescription, where a reword changes what the skill matches on — its own change, with its own review.Verification
Source for every claim about the edge entry:
schemasis requiredpackages/stack-supabase/src/create.ts:307-313configis required and carries all fourCS_*valuespackages/stack-supabase/src/wasm-inline.ts:18-30databaseUrlthrows at constructionpackages/stack-supabase/src/create.ts:358-362databaseUrlis rejected by the typepackages/stack-supabase/src/wasm-inline.ts(databaseUrl?: never).withLockContext()/.audit()throwpackages/stack-supabase/src/wasm-client-adapter.ts:40-66awaitreturns undecrypted rowsquery-builder.ts:703-725,query-results.ts:126-138packages/stack-supabase/src/create.ts:304,330Test runs, re-run at
4d717468after the rebase:pnpm run test:scripts— 941 passed, 1 skipped, 53 files (includes all three doc guards and fix(supabase): document the second entry point, and pin the runtime to the engine #952's, which now covers two more files).pnpm --filter @cipherstash/stack-supabase test:types— 56 passed, 2 files, no type errors.pnpm run code:check— 200 warnings, 59 infos, 0 errors. CI gates on errors.pnpm --filter @cipherstash/stack-supabase test(570 passed atbecdebb2) was not re-run here: this worktree has no builtindex.node, so the runtime suite fails on the missing native binding. Every commit since is documentation, a.mjsguard and that guard's file list — nothing it covers. CI builds the binding and runs it.Rebuilt
stash,@cipherstash/wizardand@cipherstash/stack-supabase.grep "cannot run in a Worker"overpackages/cli/dist/skills/andpackages/wizard/dist/skills/returns nothing; before the rebuild both carried it at line 269, so annpm packshipped it.Why
@cipherstash/wizardis bumpedskills/ships in that tarball as well asstash's —packages/wizard/tsup.config.ts:24-25copies it intodist/skills, andpackage.jsonlists that underfiles. Without the bump the published wizard keeps shipping the old text until an unrelated change moves its version. The recent skills-editing changesets werestash-only; the skills-adding ones bumped both.Related
Closes #950
Refs #912 (added the second entry point), #804 (the browser claim, which is correct and stays), #797 (
.withLockContext()on the WASM entry, which throws today).Review notes
The #953 conflict is resolved. That PR merged first, and this branch is rebased onto it. The conflict landed in
skills/stash-edge/SKILL.md's frontmatterdescriptionrather than the predicted paragraph: #953 added the browser clause there while this branch corrected "the four mandatoryCS_*variables" to "whichCS_*variables are mandatory". Both are kept. The two changes are complementary as expected — that PR explains why the browser is still ruled out, this one why the Worker no longer is.Three files carry the same stale sentence and are deliberately untouched:
packages/stack/CHANGELOG.md,packages/stack-supabase/CHANGELOG.md, anddocs/superpowers/specs/2026-07-09-supabase-v3-introspection-design.md. They are historical records and were accurate on the dates they were written.docs/reference/supabase-sdk.mdcarries it too, and is fixed in #952 instead — it is internal reference documentation and ships in no package, so it takes no changeset and did not belong in this one.Two things found and deliberately left as follow-ups:
e2e/wasm/supabase-declared.test.tspairsencryptedSupabasewith a schema from@cipherstash/stack/wasm-inline— the exact pairing this PR documents as non-compiling — hidden bydeno test --no-check. It is internal and ships to nobody, and at lines 255-284 one table object feeds both a raw wasm-inlineEncryptionand the adapter, so the fix needs two schema objects rather than a swapped import.https://claude.ai/code/session_01TvoE76yuuLn2Bmdaz8yFYy