docs(supabase): correct the stash-supabase skill#606
Conversation
…estructure)
Same defect classes as the drizzle and encryption skills, plus two of its own.
- `CREATE EXTENSION IF NOT EXISTS eql_v2;` -- no such extension. EQL is a schema
plus a composite type, installed by `stash eql install --supabase`. The skill
omitted the v2 install path entirely; added it, with `--migration` preferred
over `--direct` because a direct install does not survive `supabase db reset`.
- Encrypted columns declared `jsonb NOT NULL` in the headline schema, which the
doctrine forbids -- and which the skill itself forbids 550 lines later.
- An additive `db push` is promoted by `stash db activate`, not by cutover. The
skill said "Cutover (later) will promote it". It won't, and the cutover-time
push calls `discardPendingConfig()` first, so the additive pending is
discarded and Proxy serves a stale config for the whole dual-write window.
`db activate` appeared nowhere.
- Identity-aware encryption taught the deprecated `LockContext.identify()`
ceremony. Replaced with `OidcFederationStrategy` on `config.authStrategy` plus
a directly-constructed `LockContext`. Note that the Supabase builder's
`.withLockContext()` is typed `LockContext` only, so unlike the core
operations it will not take a bare `{ identityClaim }`.
- `error.encryptionError` is always `undefined` -- `query-builder.ts:371-374`
hardcodes it when building the error, discarding the populated value. The
skill told users to branch on it. Documented the working discriminator
(`status === 500 && statusText === 'Encryption Error'`).
- `.filter(col, op, value)` always encrypts an `equality` operand regardless of
`op`, so `.filter('age','gt',21)` cannot match. Documented, with the contrast
that a wrong index on a *declared* column errors (good), while an *undeclared*
column silently compares plaintext (bad).
- Two Linear issue IDs (`CIP-3402`) were shipping to customers in this skill.
Removed -- it is a public artifact copied into user repos.
- Repointed closed #447 at open #585; noted the known #602 authStrategy type
error; flagged that EQL v3 on Supabase installs direct-only (`--migration` is
rejected), so `supabase db reset` drops it.
|
📝 WalkthroughWalkthroughDocumentation-only update to skills/stash-supabase/SKILL.md revising database schema guidance, EQL install commands, query filter warnings, identity-aware encryption examples, error-handling notes, exported types, EQL v3 install caveats, and migration/cutover operational guidance. ChangesSKILL.md documentation revisions
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
skills/stash-supabase/SKILL.md (1)
741-749: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winClarify the SDK-only cutover workaround
stash db pushonly createspendingwhen an active config already exists; on a fresh SDK-only project it writesactive, sostash encrypt cutoverstill fails. Qualify this note with that prerequisite or spell out the missing bootstrap step.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/stash-supabase/SKILL.md` around lines 741 - 749, The SDK-only cutover note is incomplete because stash db push only creates a pending EQL config after an active config already exists. Update the guidance in the stash encrypt cutover section to call out that a fresh SDK-only project must first bootstrap the initial config before the pending-row workaround applies, and reference the stash db push / stash encrypt cutover flow so readers know when the workaround is valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@skills/stash-supabase/SKILL.md`:
- Around line 741-749: The SDK-only cutover note is incomplete because stash db
push only creates a pending EQL config after an active config already exists.
Update the guidance in the stash encrypt cutover section to call out that a
fresh SDK-only project must first bootstrap the initial config before the
pending-row workaround applies, and reference the stash db push / stash encrypt
cutover flow so readers know when the workaround is valid.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f3c2082c-a172-4058-9c18-95f4f0e219c4
📒 Files selected for processing (1)
skills/stash-supabase/SKILL.md
There was a problem hiding this comment.
Pull request overview
This PR corrects and hardens the public stash-supabase skill (skills/stash-supabase/SKILL.md) so that the guidance installed into Supabase user projects matches the actual CLI + SDK behavior (EQL install paths, schema invariants, query semantics, identity-aware encryption, and error discrimination).
Changes:
- Fixes the database schema and EQL installation guidance (nullable
jsonbencrypted columns; documentstash eql install --supabase --migration; clarify EQL is not a PG extension). - Documents known adapter behaviors and footguns (notably
.filter()always encrypting operands asequality, the declared-vs-undeclared column failure modes, and how to distinguish encryption failures givenerror.encryptionErroris alwaysundefined). - Updates identity-aware encryption docs to the current strategy-based flow (no
LockContext.identify()), adds v3 install caveats, and refreshes the documented export surface.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Encrypted columns are stored as JSONB in your Supabase database: | ||
|
|
||
| ```sql | ||
| CREATE TABLE users ( | ||
| id SERIAL PRIMARY KEY, | ||
| email jsonb NOT NULL, -- encrypted column | ||
| name jsonb NOT NULL, -- encrypted column | ||
| email jsonb, -- encrypted column | ||
| name jsonb, -- encrypted column |
There was a problem hiding this comment.
Update this to use the EQL 3.0.0 types. See https://github.com/cipherstash/encrypt-query-language/releases/tag/eql-3.0.0
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Define Encrypted Schema |
There was a problem hiding this comment.
Defining the schema explicitly is not necessary (optional).
See #588
|
|
||
| **Wrong index on a declared column → it errors.** Filtering `.gt()` on a column declared only with `.equality()` throws `Index type "..." is not configured on column "..."`, surfaced as an encryption error. That is the good case — unlike the Drizzle adapter, Supabase does not silently degrade here. | ||
|
|
||
| **Column missing from the schema → it silently compares plaintext.** If a column isn't declared in the `encryptedTable` passed to `.from(table, schema)` — a typo, or a column you forgot to add — the adapter treats it as a plaintext column, skips encryption, and sends your raw value to PostgREST to compare against a JSONB ciphertext. No error; no rows. If a filter mysteriously returns nothing, check the column is actually in the schema. |
There was a problem hiding this comment.
Automatic schema loading mitigates this problem hence that approach is preferred.
| **`ORDER BY` on encrypted columns is not currently supported** on databases without operator family support (including Supabase). | ||
|
|
||
| Without operator families installed in PostgreSQL, the database cannot sort on `eql_v2_encrypted` columns. This affects all clients — the Supabase JS SDK, Drizzle, raw SQL, and any other ORM. | ||
|
|
||
| **Workaround:** Sort application-side after decrypting the results. | ||
|
|
||
| Operator family support is currently being developed in collaboration with the Supabase and CipherStash teams and will be available in a future release. |
There was a problem hiding this comment.
This is fixed in EQLv3. OPE is used which doesn't rely on a custom op-class/family.
| .audit({ metadata: { action: "user-lookup", requestId: "abc-123" } }) | ||
| ``` | ||
|
|
||
| ## Complete Example |
There was a problem hiding this comment.
This needs to be updated for V3.
| Errors can come from Supabase (API errors) or from encryption operations. Check `error.encryptionError` for encryption-specific failures. | ||
| Errors can come from Supabase (API errors) or from encryption operations. | ||
|
|
||
| > **Don't branch on `error.encryptionError` — it is always `undefined`.** The builder's catch block hardcodes `encryptionError: undefined` when constructing the error, so the populated value is discarded even for a genuine encryption failure. Distinguish encryption failures by `status === 500 && statusText === 'Encryption Error'` instead, or use `.throwOnError()` and catch `EncryptionFailedError`. |
There was a problem hiding this comment.
This seems like a bug. We should fix and update this.
| ## EQL v3 (native `eql_v3.*` domains) | ||
|
|
||
| `encryptedSupabaseV3` is the EQL v3 counterpart of `encryptedSupabase` for | ||
| schemas authored with `@cipherstash/stack/eql/v3`. The public surface and call | ||
| shape are **identical to v2** — same filter methods, `withLockContext`, | ||
| `audit` — only the schema type and the wire encoding differ. Columns are | ||
| stored in their native `eql_v3.*` domain (a `DOMAIN … AS jsonb` with a CHECK) | ||
| instead of the v2 composite `eql_v2_encrypted`. | ||
|
|
||
| ### Setup | ||
|
|
||
| ```typescript | ||
| import { Encryption } from "@cipherstash/stack" | ||
| import { encryptedTable, types } from "@cipherstash/stack/eql/v3" | ||
| import { encryptedSupabaseV3 } from "@cipherstash/stack/supabase" | ||
|
|
||
| const users = encryptedTable("users", { | ||
| email: types.TextSearch("email"), // eql_v3.text_search — eq + range + free-text | ||
| amount: types.IntegerOrd("amount"), // eql_v3.integer_ord — eq + range | ||
| joined: types.TimestampOrd("joined_at") // eql_v3.timestamp_ord — eq + range, decrypts to Date | ||
| }) |
There was a problem hiding this comment.
This section needs to be updated. The EQL V3 type names have changed. Supabase integration loads schema automatically, and the V3 stuff should be the focus of the skill. Move it to the top and consider removing V2, or moving V2 to a separate skill.
|
|
||
| - **INTERIM — filter operands are full envelopes.** This is a workaround, not | ||
| the design (tracked as Linear **CIP-3402**). Why it is required today: every | ||
| the design; a term-only query envelope is planned. Why it is required today: every |
There was a problem hiding this comment.
This has now been fixed.
Why
skills/stash-supabase/SKILL.mdships in thestashtarball andstash initinstalls it into every Supabase project. Same defect classes as #598 (encryption) and #604 (drizzle), plus two of its own.This is the correctness pass. The restructure you asked for on #598 (local-dev section, v3 focus, migration split, cross-references) is a separate follow-up, built on the
feat/eql-v3-text-search-schemabranch.Defects
Two Linear issue IDs (
CIP-3402) were shipping to customers in this skill. It's a public artifact copied into user repos. Removed.CREATE EXTENSION IF NOT EXISTS eql_v2;— no such extension.eql_v2is a schema pluspublic.eql_v2_encrypted, installed by the CLI. The skill omitted the v2 install path entirely; it now documentsstash eql install --supabase --migration, and says why--migrationbeats--direct(a direct install does not survivesupabase db reset).Encrypted columns declared
jsonb NOT NULLin the headline schema — forbidden by invariant 1 of the doctrine that ships alongside this skill. The skill even states the rule correctly 550 lines later, and then contradicts it in its own opening example.An additive
db pushis promoted bystash db activate, not by cutover. The skill said "Cutover (later) will promote it." It won't — cutover promotes only the rename pending — and the cutover-timedb pushcallsdiscardPendingConfig()before writing its own (push.ts:119), so the un-activated additive pending is discarded and Proxy serves a stale config for the whole dual-write window.db activateappeared nowhere in the skill. Same bug as #604.Identity-aware encryption taught the deprecated
LockContext.identify()ceremony. Per-operation CTS tokens were removed inprotect-ffi0.25. Replaced withOidcFederationStrategyonconfig.authStrategyplus a directly-constructedLockContext. Documented a real asymmetry: the Supabase builder's.withLockContext()is typedLockContextonly, so unlike the core operations it will not accept a bare{ identityClaim }— verified withtsc.error.encryptionErroris alwaysundefined. The skill told users to branch on it.query-builder.ts:371-374hardcodesencryptionError: undefinedwhen constructing the error, discarding the populated value even for a genuineEncryptionFailedError. Documented the working discriminator (status === 500 && statusText === 'Encryption Error'), and flagged the code bug..filter(col, op, value)always encrypts anequalityoperand regardless ofop, so.filter('age','gt',21)builds an equality term for a range operator and cannot match. Documented, along with the two failure modes that differ from Drizzle: a wrong index on a declared column errors (good), while an undeclared column silently compares plaintext against JSONB (bad, and previously undocumented).Also
stash db pushwhen not using Proxy #447 → open EQL v3: make it the default, and stop the CLI recommendingdb push(a v2/Proxy-only step) #585.authStrategytype error (config.authStrategy doesn't typecheck: protect-ffi's AuthStrategy type is narrower than its runtime (auth 0.41 Result envelope) #602) so the identity snippet is honest about not compiling until protectjs-ffi#131 releases.--migrationis rejected under--eql-version 3, sosupabase db resetdrops EQL. Not previously stated, and this skill's own lifecycle usesdb reset.V3FilterableKeys,EncryptedSupabaseResponse,EncryptedSupabaseErrorexports.Verification
tsconfig.json(withpathsandcustomConditions: ["node"]).new LockContext()andnew LockContext({ context: { identityClaim: [...] } })compile;{ identityClaim: ['sub'] }does not — which is why the skill now says so. Only the known config.authStrategy doesn't typecheck: protect-ffi's AuthStrategy type is narrower than its runtime (auth 0.41 Result envelope) #602authStrategyerror remains.--supabase --eql-version 3selectscipherstash-encrypt-v3-supabase.sqland creates schemaeql_v3(ranloadBundledEqlSql).eql-3.0.0-alpha.2, and that botheql_v3andeql_v3_internalschemas are created.discardPendingConfig()runs before the new pending insert.select('*')rejection,::jsonbcasts, and everyResultsnippet narrowing.failurebefore.data.supabase-v3.test.tsfails onmaintoo (needs a live Postgres with ananonrole) — pre-existing, unrelated.Summary by CodeRabbit