Skip to content

feat(stack,cli): EQL v3 Supabase adapter (encryptedSupabaseV3) + v3 install path#547

Open
freshtonic wants to merge 4 commits into
feat/eql-v3-types-modulefrom
james/cip-3300-spike-integrate-eql-v3-into-supabase-orm
Open

feat(stack,cli): EQL v3 Supabase adapter (encryptedSupabaseV3) + v3 install path#547
freshtonic wants to merge 4 commits into
feat/eql-v3-types-modulefrom
james/cip-3300-spike-integrate-eql-v3-into-supabase-orm

Conversation

@freshtonic

Copy link
Copy Markdown
Contributor

Summary

Implements EQL v3 on Supabase per the design spec in #546 (CIP-3300): encryptedSupabaseV3 at parity with the v2 encryptedSupabase adapter, using native eql_v3.* domain columns. Stacked on feat/eql-v3-types-module (#541), targeting that branch.

Core principle preserved from the spec: v3 mirrors v2 exactly — same query mechanism (direct EQL operators over PostgREST), same operator-visibility caveat + Exposed-schemas requirement, same install/permissions story. Only the column types and the wire encoding differ.

What's here

SDK adapter (@cipherstash/stack/supabase)

  • encryptedSupabaseV3 + EncryptedQueryBuilderV3Impl, a narrow subclass of EncryptedQueryBuilderImpl. The base class gains protected dialect seams whose defaults preserve v2 byte-for-byte (pinned by v2 regression tests); the v3 subclass overrides: column recognition, property↔DB name resolution (buildColumnKeyMap, aliased prop:db::jsonb select casts), raw-jsonb mutation payloads (no composite wrap), full-envelope filter operands, like/ilike → PostgREST cs, and Date reconstruction from cast_as.
  • Typing: rows default to InferPlaintext<Table> & Record<string, unknown>; with an explicit row type, storage-only columns (e.g. types.Bool) are excluded from filter methods at the type level (runtime guard always applies). The shared EncryptedQueryBuilder gains an optional filterable-keys generic (defaulted — v2 signatures unchanged).

DB bundle + install

  • v3 bundles vendored into packages/cli/src/sql/ via a checked-in derivation script (fallback strategy from the spec; the Supabase variant strips the two opclass chunks at their --! @file markers, mirroring upstream's **/*operator_class.sql exclusion glob; sync risk documented in the script).
  • stash db install --eql-version 3 [--supabase] — direct path only for now (--drizzle/--migration/--latest rejected with clear errors). Grants generalized: supabasePermissionsSql(schemaName) shared by v2/v3 (SUPABASE_PERMISSIONS_SQL unchanged, SUPABASE_PERMISSIONS_SQL_V3 added).
  • installEqlV3IfNeeded (test helper) is Supabase-aware: { supabase: true } = stripped bundle + eql_v3 grants.

Tests

  • 18 wire-encoding unit tests (mock encryption + supabase clients, run in CI without live creds) covering both dialects incl. v2 regression pins for every seam.
  • 7 type-level tests (supabase-v3.test-d.ts).
  • Live suite supabase-v3.test.ts mirroring the v2 suite (same env gating): round-trips incl. a Timestamptz column proving Date reconstruction, bulk models, text_search equality, free-text likecs, int4_ord equality + range, timestamptz_ord range with Date values.
  • v2 range baseline added to supabase.test.ts (gte/lte on an orderAndRange number column) — the spec flagged that encrypted range on Supabase had no CI-covered v2 baseline.
  • CLI: +7 installer/flag-validation tests (44 total pass).

Docs

  • stash-supabase skill: new EQL v3 section (setup, DDL mapping, install, the Exposed-schemas silent-fallback warning, v3 behaviour, shared caveats).
  • Recreated docs/reference/supabase-sdk.md (deleted in def9f4bd; fixes the dangling AGENTS.md link).
  • Changeset: minor for @cipherstash/stack + stash.

Deviations from the spec (found in the bundle source, not guessed)

  1. Full-envelope operands are required for every domain, not just text_search equality. The spec assumed single-capability domains' terms satisfy their own CHECKs (e.g. "an int4_ord range term carries ob, which is exactly what int4_ord requires"). They don't: every eql_v3.* domain CHECK also requires v, i, and c (see e.g. the int4_ord / text_eq CHECKs in the bundle), and protect-ffi's EncryptedScalarQuery is typed c?: never — query terms can never pass any domain CHECK. The (domain, jsonb) operator functions also cast their operand into the domain (b::eql_v3.text_search), so there is no coercion-free path. The adapter therefore encrypts all filter operands with the storage path; the operators extract the term they need (eq_term/ord_term/match_term). This also resolves the spec's open question fix(ci): set up bun in github actions for release #2 — the full envelope isn't just the cleanest way, it's the only way.
  2. like/ilike cannot stay like on the wire. The v3 domains define =,<>,<,<=,>,>=,@>,<@ but no ~~. Encrypted pattern filters are emitted as PostgREST cs (@> on match_term). Match is tokenized + downcased so like/ilike are equivalent; % wildcards should not be used.
  3. Free-text search needs include_original: false on the column's match index: with the default true, the full-envelope operand's bloom carries the whole pattern as an extra token, so substring patterns only match when equal to the stored value. Documented in the skill/reference; the live test's schema sets it explicitly with a load-bearing comment.

Verification

  • packages/stack: 0 src/ type errors; 25 unit tests + 7 type tests pass; live suites parse and gate correctly (skipped without env). Pre-existing __tests__ type errors and Not authenticated live-test failures on this branch are unchanged (no local CS_*/Supabase creds — same count at baseline).
  • packages/cli: typecheck error count identical to baseline (all pre-existing, unbuilt workspace deps); 44 tests pass; both packages tsup build cleanly, v3 bundles ship in dist/sql/.
  • Not verified here: the live Supabase suites need a project with eql_v3 in Exposed schemas + CS_* creds. The full-envelope equality behaviour matches the spec's live spike; the range/free-text assertions should be confirmed on the first live run.

Out of scope (per spec)

Plaintext→encrypted migration lifecycle; encrypted ORDER BY (OPE terms are the forward path); v3 in the Drizzle/Supabase-migration-file install paths; the spec's proposed install-time exposed-schema probe (documented as a firm follow-up — worth its own issue since it retroactively protects v2 too).

Closes CIP-3300. Design: #546.

…pter

The v2 query mechanism (direct EQL operators over PostgREST) unchanged;
EncryptedQueryBuilderImpl gains narrow protected seams whose defaults
preserve the v2 behaviour byte-for-byte, and a v3 subclass overrides them:

- column recognition + property↔DB name resolution via buildColumnKeyMap
  (filters, mutations, aliased select casts `prop:db::jsonb`)
- raw jsonb mutation payloads (no eql_v2 composite wrap)
- full-envelope filter operands: every eql_v3.* domain CHECK requires the
  storage keys (v/i/c + index terms) and the SQL operators coerce their
  jsonb operand into the domain, so a narrowed encryptQuery term (c?: never)
  fails 23514 on EVERY domain — not just text_search as the design spec
  assumed. All operands go through encrypt() instead.
- like/ilike on encrypted columns → PostgREST cs (bloom @>); the domains
  define no LIKE operator
- Date reconstruction from cast_as on decrypted rows
- capability validation: filters on storage-only columns or unsupported
  query types throw typed + runtime errors

Wire-encoding unit tests (mock encryption + supabase clients) cover both
dialects, including v2 regression pins for the seams.

Part of CIP-3300; design spec in PR #546.
- Vendor the v3 SQL bundles into packages/cli/src/sql via a checked-in
  derivation script (scripts/build-eql-v3-sql.mjs): the full bundle is a
  byte-identical copy of the stack fixture monolith; the Supabase variant
  strips the two CREATE OPERATOR CLASS/FAMILY chunks at their --! @file
  markers, mirroring upstream's **/*operator_class.sql exclusion glob.
  Temporary vendoring (sync risk documented) until upstream ships v3
  release artifacts.
- EQLInstaller: eqlVersion option on install/isInstalled/
  getInstalledVersion; v3 + --latest rejected (no public artifacts);
  grants keyed to the installed schema via the new
  supabasePermissionsSql(schemaName) helper (SUPABASE_PERMISSIONS_SQL
  unchanged, SUPABASE_PERMISSIONS_SQL_V3 added).
- stash db install --eql-version 3: direct install only for now —
  explicit --drizzle/--migration/--latest are rejected up-front,
  auto-detected drizzle falls back to direct with a notice.

Part of CIP-3300.
- supabase-v3.test.ts mirrors the v2 live suite over eql_v3 domains:
  round-trips (incl. a Timestamptz column proving Date reconstruction),
  bulk models, text_search equality (full-envelope operand), free-text
  like→cs (include_original: false — load-bearing, see the suite header),
  int4_ord equality + gte/lte range, timestamptz_ord range with Date
  values. Same env gating as the v2 suite; the eql_v3 Exposed-schemas
  dashboard step is documented as the manual prerequisite.
- supabase.test.ts gains the v2 encrypted-range test (gte/lte on an
  orderAndRange number column) — the 'range filtering works on Supabase'
  claim previously rested on a one-off live spike with no CI baseline.
- installEqlV3IfNeeded accepts { supabase: true }: opclass-stripped
  bundle + eql_v3 grants, matching the CLI's --eql-version 3 --supabase.

Part of CIP-3300.
- stash-supabase skill: new 'EQL v3 (native eql_v3.* domains)' section —
  setup, per-domain DDL, --eql-version 3 install, the Exposed-schemas
  silent-fallback warning, v3-specific behaviour (full-envelope operands,
  like→cs, include_original: false for substring match), shared caveats.
- Recreate docs/reference/supabase-sdk.md (deleted in def9f4b; AGENTS.md
  'Useful Links' had a dangling reference) covering both adapters, the
  install + Exposed-schemas story, and the v3 encoding details.
- Changeset: minor for @cipherstash/stack and stash.

Part of CIP-3300.
@freshtonic freshtonic requested a review from a team as a code owner July 3, 2026 14:16
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cd6620

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@cipherstash/stack Minor
stash Minor
@cipherstash/bench Patch
@cipherstash/prisma-next Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e Patch

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

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 319bd23d-e3f1-44cf-af4c-3a5d006af85d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch james/cip-3300-spike-integrate-eql-v3-into-supabase-orm

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.

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.

1 participant