Skip to content

docs(supabase): correct the stash-supabase skill#606

Open
coderdan wants to merge 1 commit into
mainfrom
docs/stash-supabase-skill-refresh
Open

docs(supabase): correct the stash-supabase skill#606
coderdan wants to merge 1 commit into
mainfrom
docs/stash-supabase-skill-refresh

Conversation

@coderdan

@coderdan coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Why

skills/stash-supabase/SKILL.md ships in the stash tarball and stash init installs 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-schema branch.

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_v2 is a schema plus public.eql_v2_encrypted, installed by the CLI. The skill omitted the v2 install path entirely; it now documents stash eql install --supabase --migration, and says why --migration beats --direct (a direct install does not survive supabase db reset).

Encrypted columns declared jsonb NOT NULL in 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 push is promoted by stash 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-time db push calls discardPendingConfig() 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 activate appeared nowhere in the skill. Same bug as #604.

Identity-aware encryption taught the deprecated LockContext.identify() ceremony. Per-operation CTS tokens were removed in protect-ffi 0.25. Replaced with OidcFederationStrategy on config.authStrategy plus a directly-constructed LockContext. Documented a real asymmetry: the Supabase builder's .withLockContext() is typed LockContext only, so unlike the core operations it will not accept a bare { identityClaim } — verified with tsc.

error.encryptionError is always undefined. The skill told users to branch on it. query-builder.ts:371-374 hardcodes encryptionError: undefined when constructing the error, discarding the populated value even for a genuine EncryptionFailedError. Documented the working discriminator (status === 500 && statusText === 'Encryption Error'), and flagged the code bug.

.filter(col, op, value) always encrypts an equality operand regardless of op, 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

Verification

  • Typechecked the identity snippet against the package's real tsconfig.json (with paths and customConditions: ["node"]). new LockContext() and new 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) #602 authStrategy error remains.
  • Confirmed --supabase --eql-version 3 selects cipherstash-encrypt-v3-supabase.sql and creates schema eql_v3 (ran loadBundledEqlSql).
  • Confirmed the vendoring commit pins eql-3.0.0-alpha.2, and that both eql_v3 and eql_v3_internal schemas are created.
  • Confirmed discardPendingConfig() runs before the new pending insert.
  • Audit verified the rest is accurate: every filter method, the v3 domain names and capabilities, transparent decrypt-on-select, select('*') rejection, ::jsonb casts, and every Result snippet narrowing .failure before .data.

supabase-v3.test.ts fails on main too (needs a live Postgres with an anon role) — pre-existing, unrelated.

Summary by CodeRabbit

  • Documentation
    • Clarified encrypted database schema guidance and updated setup steps for EQL installation.
    • Added clearer warnings about query filtering behavior, failure cases, and encryption error handling.
    • Expanded identity-aware encryption examples and troubleshooting notes.
    • Updated migration and cutover instructions, including activation steps and version-specific setup details.

…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.
@coderdan coderdan requested a review from a team as a code owner July 9, 2026 10:07
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 015e8da

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Documentation-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.

Changes

SKILL.md documentation revisions

Layer / File(s) Summary
Schema and EQL install guidance
skills/stash-supabase/SKILL.md
Changes encrypted-column schema example to nullable jsonb and replaces extension-based EQL v2 setup with stash eql install --supabase --migration CLI flow.
Query filter behavior warnings
skills/stash-supabase/SKILL.md
Adds warning that .filter() always encrypts operands as equality terms and clarifies two failure modes for index/schema mismatches.
Identity-aware encryption example rewrite
skills/stash-supabase/SKILL.md
Rewrites example to use OidcFederationStrategy and .withLockContext(), adds lock-context matching requirements and type workaround notes.
Error handling and exported types
skills/stash-supabase/SKILL.md
Clarifies error.encryptionError is always undefined and expands exported types list with EncryptedSupabaseResponse/EncryptedSupabaseError.
EQL v3 install and query envelope notes
skills/stash-supabase/SKILL.md
Adds v3 direct-path-only install caveat and updates term-only scalar query envelope explanations.
Migration and cutover operational guidance
skills/stash-supabase/SKILL.md
Adds stash db activate steps, pending/activation behavior notes, and updates SDK-only cutover known-gap note.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • cipherstash/stack#448: Adds the same "stash encrypt cutover requires pending EQL config" workaround note referenced in this PR's SDK-only users guidance.
  • cipherstash/stack#543: Corresponds to the CLI rename from stash db install to stash eql install reflected in this PR's install command update.

Suggested reviewers: calvinbrewer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the PR’s main docs correction for the stash-supabase skill.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/stash-supabase-skill-refresh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Clarify the SDK-only cutover workaround stash db push only creates pending when an active config already exists; on a fresh SDK-only project it writes active, so stash encrypt cutover still 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ac66a8 and 015e8da.

📒 Files selected for processing (1)
  • skills/stash-supabase/SKILL.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 jsonb encrypted columns; document stash eql install --supabase --migration; clarify EQL is not a PG extension).
  • Documents known adapter behaviors and footguns (notably .filter() always encrypting operands as equality, the declared-vs-undeclared column failure modes, and how to distinguish encryption failures given error.encryptionError is always undefined).
  • 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.

Comment on lines +26 to +32
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## Setup

### 1. Define Encrypted Schema

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic schema loading mitigates this problem hence that approach is preferred.

Comment on lines 295 to 301
**`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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a bug. We should fix and update this.

Comment on lines 449 to 469
## 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
})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has now been fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants