From 524903cf4582009b86b1d8f6632900b8f4a4b4b5 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Mon, 20 Jul 2026 12:37:20 +1000 Subject: [PATCH 1/2] fix(skills): correct stale EQL v3 guidance in bundled skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @cipherstash/migrate and `stash encrypt *` gained EQL v3 support (#648, closed 2026-07-17), but the shipped skills still said the rollout tooling was v2-only. These skills are copied into customer repos, so the stale text steered users away from v3 toward workarounds they no longer need. - stash-drizzle, stash-supabase: replace the "v3 not supported end-to-end" callouts with an accurate EQL version note (auto-detection by Postgres domain type; v3 has no cut-over rename). - stash-supabase: drop the "Interim path until #648: the v2 encrypted twin" section — a v2 twin is no longer needed for CLI-managed backfill. - stash-drizzle, stash-supabase: document that `stash encrypt drop` targets the original column under v3, `_plaintext` under v2. - stash-cli: EQLInstaller `eqlVersion` defaults to 3, not 2 (DEFAULT_EQL_VERSION, installer/index.ts:69); reword the v2 cut-over gap note that cited #585 as open tracking. Verified `stash manifest --json` resolves every command/flag named in stash-cli. code:check clean (errors); changeset added. --- .changeset/skills-eql-v3-accuracy.md | 25 +++++++++ skills/stash-cli/SKILL.md | 4 +- skills/stash-drizzle/SKILL.md | 32 +++++++---- skills/stash-supabase/SKILL.md | 80 +++++++++------------------- 4 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 .changeset/skills-eql-v3-accuracy.md diff --git a/.changeset/skills-eql-v3-accuracy.md b/.changeset/skills-eql-v3-accuracy.md new file mode 100644 index 000000000..75c9f55de --- /dev/null +++ b/.changeset/skills-eql-v3-accuracy.md @@ -0,0 +1,25 @@ +--- +'stash': patch +--- + +Correct stale EQL v3 guidance in the bundled agent skills. + +`@cipherstash/migrate` and the `stash encrypt *` commands gained EQL v3 support +(cipherstash/stack#648, now closed), but the shipped skills still told readers the +rollout tooling was v2-only. Since these skills are copied into customer repos, the +stale text steered users away from v3 and toward workarounds they no longer need. + +- **`stash-drizzle`, `stash-supabase`** — replaced the "v3 not supported end-to-end" + callouts with an accurate EQL version note: the tooling auto-detects a column's + generation from its Postgres domain type, and the two lifecycles differ at the end. + v3 is `backfill → switch the app to the encrypted column by name → drop` with no + cut-over rename; v2 keeps the `stash encrypt cutover` rename plus config promotion. +- **`stash-supabase`** — removed the "Interim path until #648: the v2 encrypted twin" + section; a v2 twin is no longer needed to get CLI-managed backfill. +- **`stash-drizzle`, `stash-supabase`** — the drop step now documents that + `stash encrypt drop` targets the *original* column under v3 (there is no + `_plaintext`, since nothing was renamed) and `_plaintext` under v2. +- **`stash-cli`** — corrected the documented `EQLInstaller` default: `eqlVersion` + defaults to `3`, not `2`, matching the `--eql-version` CLI default. Also reworded + the v2 cut-over known-gap note, which cited cipherstash/stack#585 as open tracking + when it was resolved by making v3 the default. diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index 8fe14a531..63f9e66d8 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -508,7 +508,7 @@ In one transaction it renames `` → `_plaintext` and `_encrypted > **After cutover, `` holds ciphertext — the read path is not automatic.** Wire reads through the encryption client (`decryptModel(row, table)` for Drizzle, the `encryptedSupabaseV3` wrapper for Supabase — `encryptedSupabase` on the legacy v2 surface, otherwise `decrypt` / `bulkDecryptModels`) before returning values to callers. Skip this and your read paths hand raw EQL payloads to end users. The integration skill has the exact API. **CipherStash Proxy is the one exception** — it decrypts on the wire, so Proxy users need no application change. The cutover plan written by `stash plan` includes this read-path switch as an explicit step. > -> **Known gap (v2).** The pending-configuration precondition is satisfied by `stash db push`. SDK-only users (who otherwise never need `db push`) must therefore run it once before `encrypt cutover`. (EQL v3 columns sidestep this entirely — no configuration table, no cutover; see above.) Tracked in [issue #585](https://github.com/cipherstash/stack/issues/585). +> **Known gap (v2).** The pending-configuration precondition is satisfied by `stash db push`. SDK-only users (who otherwise never need `db push`) must therefore run it once before `encrypt cutover`. This gap is specific to the legacy v2 path and is not being decoupled — EQL v3 columns sidestep it entirely (no configuration table, no cut-over; see above), which is how [issue #585](https://github.com/cipherstash/stack/issues/585) was resolved when v3 became the default. Flags: `--table`, `--column`, `--proxy-url `, `--migrations-dir `. @@ -604,7 +604,7 @@ await installer.getInstalledVersion() // string | 'unknown' | null await installer.install({ supabase: true }) // executes in a transaction ``` -`isInstalled`, `getInstalledVersion`, and `install` all accept `eqlVersion?: 2 | 3` (default `2`), selecting the `eql_v2` or `eql_v3` schema. `install` also takes `excludeOperatorFamily`, `supabase`, and `latest` (v2 only). +`isInstalled`, `getInstalledVersion`, and `install` all accept `eqlVersion?: 2 | 3` (default `3`, matching the CLI's `--eql-version` default), selecting the `eql_v3` or `eql_v2` schema. `install` also takes `excludeOperatorFamily`, `supabase`, and `latest` (v2 only). ```typescript type PermissionCheckResult = { diff --git a/skills/stash-drizzle/SKILL.md b/skills/stash-drizzle/SKILL.md index 3c2a6ebbd..7bcacd872 100644 --- a/skills/stash-drizzle/SKILL.md +++ b/skills/stash-drizzle/SKILL.md @@ -365,9 +365,9 @@ if (!decrypted.failure) { The hard case: a Drizzle table that already exists in production with live data in a plaintext column you want to encrypt. You can't just change the column type — that would drop the data and break NOT NULL constraints. -CipherStash splits this into two named steps with a hard production-deploy gate between them: an **encryption rollout** (schema-add + dual-write code) and an **encryption cutover** (backfill + rename + drop). (If using CipherStash Proxy, the rollout also includes `stash db push` to register the encryption config with EQL.) The `stash-encryption` skill is the canonical reference for the lifecycle; this section walks the Drizzle-specific shape. +CipherStash splits this into two named steps with a hard production-deploy gate between them: an **encryption rollout** (schema-add + dual-write code) and a **cutover step** (backfill + switch reads + drop — under EQL v2 the switch is a rename, under v3 it is an application-side change). (If using CipherStash Proxy, the rollout also includes `stash db push` to register the encryption config with EQL.) The `stash-encryption` skill is the canonical reference for the lifecycle; this section walks the Drizzle-specific shape. -> **⚠️ v3 backfill tooling status.** The CLI backfill/cutover tooling (`stash encrypt backfill`, `stash encrypt cutover`, and the underlying `@cipherstash/migrate`) currently targets **EQL v2 columns**. v3 compatibility is tracked in [cipherstash/stack#648](https://github.com/cipherstash/stack/issues/648). The lifecycle below (schema-add → dual-write → deploy gate → backfill → cutover → drop) is the correct shape for v3 either way — until #648 lands, run the backfill/rename steps with your own scripts (encrypt with `bulkEncryptModels`, write in chunks) instead of the `stash encrypt` commands. +> **EQL version note.** The CLI rollout tooling (`stash encrypt *`, and the underlying `@cipherstash/migrate`) works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what the schema below uses) is `schema-add → dual-write → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. > **Where am I?** Run `stash status` first (substitute the runner per the note above). It shows you which Drizzle tables/columns are mid-rollout, which are post-deploy, and what the next move is. Re-run after every transition. @@ -474,8 +474,6 @@ Once dual-writes are live in production and `cs_migrations` records `dual_writin #### Backfill: encrypt the historical rows -> Until [#648](https://github.com/cipherstash/stack/issues/648) lands, the commands in this step target v2 columns — for v3 columns, replicate the same shape with a script (chunked `bulkEncryptModels` + UPDATE inside transactions, resumable and idempotent). - ```bash stash encrypt backfill --table users --column email # (Interactive: answer 'yes' to the dual-write confirmation prompt.) @@ -486,9 +484,18 @@ Resumable, idempotent, chunked. The CLI walks the table in keyset-pagination ord If something goes wrong (e.g. you discover the dual-write code wasn't actually live when backfill ran), re-run with `--force` to re-encrypt every row regardless of current state. -> **SDK-only note:** `stash encrypt cutover` currently requires a pending EQL configuration set by `stash db push`. If you're using the SDK without Proxy, you'll hit a "No pending EQL configuration" error from cutover. **Workaround:** run `stash db push` once before `stash encrypt cutover`. +> **SDK-only note (EQL v2 only):** `stash encrypt cutover` requires a pending EQL configuration set by `stash db push`. If you're using the SDK without Proxy, you'll hit a "No pending EQL configuration" error from cutover. **Workaround:** run `stash db push` once before `stash encrypt cutover`. EQL v3 columns never hit this — cut-over doesn't apply to them. + +#### Switch reads to the encrypted column + +**EQL v3 (the schema above): there is no cut-over.** The encrypted column keeps +its own name — you switch the application to it by name, verify reads, then drop +the plaintext column. Point your queries at `email_encrypted`, deploy, and +confirm reads decrypt correctly; then skip ahead to the drop step. Running +`stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. -#### Cutover: rename swap and activate +The rest of this subsection is the **EQL v2** path (a `eql_v2_encrypted` twin), +kept for existing v2 deployments. First, update the Drizzle schema to the post-cutover shape — switch `email` to the encrypted type and remove the `email_encrypted` column. @@ -547,17 +554,22 @@ Once read paths are updated and you're confident reads are decrypting correctly, stash encrypt drop --table users --column email ``` -The CLI emits a Drizzle migration file with `ALTER TABLE users DROP COLUMN email_plaintext;`. Review and apply with `drizzle-kit migrate`. Update the schema to remove `email_plaintext`: +The CLI emits a Drizzle migration file with the drop. **Which column it drops depends on the EQL version**, which the CLI auto-detects: + +- **v3** — drops the original plaintext column, `ALTER TABLE users DROP COLUMN email;`. There was no rename, so no `email_plaintext` exists. Requires the column to be in the `backfilled` phase, plus a live coverage check. +- **v2** — drops the post-rename leftover, `ALTER TABLE users DROP COLUMN email_plaintext;`. Requires the `cut-over` phase. + +Review and apply with `drizzle-kit migrate`, then update the schema to its final shape — the encrypted column is the only one left: ```typescript -// src/db/schema.ts (final) +// src/db/schema.ts (final, EQL v3) export const users = pgTable('users', { id: integer('id').primaryKey().generatedAlwaysAsIdentity(), - email: types.TextSearch('email'), + email_encrypted: types.TextSearch('email_encrypted'), }) ``` -Also remove the dual-write code from app paths — `email_plaintext` is gone; only `email` (encrypted) is written now. +Also remove the dual-write code from app paths — the plaintext column is gone; only the encrypted column is written now. ### Inspecting progress at any time diff --git a/skills/stash-supabase/SKILL.md b/skills/stash-supabase/SKILL.md index 7eb38bd5f..c29614942 100644 --- a/skills/stash-supabase/SKILL.md +++ b/skills/stash-supabase/SKILL.md @@ -627,7 +627,7 @@ The hard case: a Supabase table that already exists with live data in a plaintex CipherStash splits this into two named steps with a hard production-deploy gate between them: an **encryption rollout** (schema-add + dual-write code) and an **encryption cutover** (backfill + rename + drop). The `stash-encryption` skill is the canonical reference for the lifecycle; this section walks the Supabase-specific shape. -> **Known gap (EQL v3 backfill).** The `stash encrypt backfill` / `stash encrypt cutover` tooling currently targets EQL v2 (`eql_v2_encrypted`) columns — EQL v3 domain columns are not yet supported end-to-end. Tracked in [cipherstash/stack#648](https://github.com/cipherstash/stack/issues/648). The lifecycle below (rollout → deploy gate → cutover) is the correct shape either way; until #648 lands, run the CLI backfill/cutover commands against a v2 encrypted twin (see "Interim path until #648" just below), or backfill v3 columns with your own script. +> **EQL version note.** The `stash encrypt *` tooling works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what this section's schema uses) is `rollout → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. > **Using CipherStash Proxy?** If you query encrypted data through [CipherStash Proxy](https://github.com/cipherstash/proxy) instead of the SDK, also run `stash db push` after schema-add and again before cutover to register the encrypted column shape with EQL. @@ -635,50 +635,6 @@ CipherStash splits this into two named steps with a hard production-deploy gate > **Where am I?** Run `stash status` first (substitute the runner per the note above). It shows you which tables/columns are mid-rollout, which are post-deploy, and what the next move is. Re-run after every transition. -### Interim path until #648: the v2 encrypted twin - -**This is the interim EQL v2 path, distinct from the v3 surface the rest of -this skill documents.** `stash encrypt backfill` / `stash encrypt cutover` -only operate on `eql_v2_encrypted` columns today, so to get the CLI-managed -backfill/cutover, make the twin a **v2** column and dual-write it through the -legacy `encryptedSupabase` (v2) wrapper. The lifecycle below (rollout → deploy -gate → cutover) is unchanged — only the twin's column type and the dual-write -client differ: - -```sql --- schema-add: v2 twin instead of the v3 domain (still nullable) -ALTER TABLE users - ADD COLUMN email_encrypted eql_v2_encrypted; -``` - -```typescript -// Dual-write through the legacy v2 wrapper: hand-written client-side schema, -// two-argument from(table, schema). -import { Encryption } from '@cipherstash/stack' -import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema' -import { encryptedSupabase } from '@cipherstash/stack-supabase' - -const users = encryptedTable('users', { - email_encrypted: encryptedColumn('email_encrypted').equality().freeTextSearch(), -}) - -const encryptionClient = await Encryption({ schemas: [users] }) -const esV2 = encryptedSupabase({ supabaseClient, encryptionClient }) - -// Dual-write: BOTH the plaintext column and the encrypted twin. -export async function insertUser(email: string) { - return esV2.from('users', users).insert({ - email, // plaintext — keep writing until drop - email_encrypted: email, // v2 twin — encrypted automatically - }) -} -``` - -With the v2 twin in place, the backfill and cutover commands in Step 2 work -as written. Alternatively, keep the twin v3 (as shown in the steps below) and -backfill it with your own script — once #648 lands, the v3 twin becomes the -CLI-supported path end to end. - ### Starting state You have: @@ -786,11 +742,21 @@ stash encrypt backfill --table users --column email # (CI: pass --confirm-dual-writes-deployed instead.) ``` -Resumable, idempotent, chunked. The CLI walks the table in keyset-pagination order, encrypts each chunk via the encryption client, and writes the ciphertext into `email_encrypted` inside transactions that also checkpoint to `cs_migrations`. SIGINT-safe. (Remember the known-gap callout above: today this command targets EQL v2 columns — see #648.) +Resumable, idempotent, chunked. The CLI walks the table in keyset-pagination order, encrypts each chunk via the encryption client, and writes the ciphertext into `email_encrypted` inside transactions that also checkpoint to `cs_migrations`. SIGINT-safe. It auto-detects whether the column is EQL v2 or v3 and records that in `cs_migrations`. If something goes wrong (e.g. you discover the dual-write code wasn't actually live when backfill ran), re-run with `--force` to re-encrypt every row regardless of current state. -#### Cutover: rename swap and activate +#### Switch reads to the encrypted column + +**EQL v3 (the schema above): there is no cut-over.** The encrypted column keeps +its own name — point your application at `email_encrypted` through the +`encryptedSupabaseV3` wrapper, deploy, verify reads decrypt correctly, then skip +ahead to the drop step. Running `stash encrypt cutover` on a v3 column reports +"not applicable" and exits 0. + +The rest of this subsection is the **EQL v2** path (an `eql_v2_encrypted` twin +queried through the legacy `encryptedSupabase` wrapper), kept for existing v2 +deployments. First, if you use declared `schemas`, update them to the post-cutover shape — the encrypted column will live under the original column name: @@ -803,7 +769,7 @@ export const users = encryptedTable('users', { (Without declared schemas, introspection picks up the renamed column at the next client startup.) -> **Known gap (SDK-only users):** `stash encrypt cutover` currently requires a pending EQL configuration, which is set by `stash db push`. If you're using the SDK without Proxy, you'll hit a "No pending EQL configuration" error from cutover. **Workaround:** run `stash db push` once before `stash encrypt cutover`. This will be decoupled in a future release (tracked separately). +> **Known gap (EQL v2, SDK-only users):** `stash encrypt cutover` requires a pending EQL configuration, which is set by `stash db push`. If you're using the SDK without Proxy, you'll hit a "No pending EQL configuration" error from cutover. **Workaround:** run `stash db push` once before `stash encrypt cutover`. EQL v3 columns never hit this — cut-over doesn't apply to them. > > **Using CipherStash Proxy?** Re-push the encryption config so EQL has a pending row that points at `email` (no `_encrypted` suffix): > @@ -843,7 +809,12 @@ Once read paths are routing through the wrapper and you're confident reads are d stash encrypt drop --table users --column email ``` -The CLI emits a Supabase migration file with `ALTER TABLE users DROP COLUMN email_plaintext;`. Review and apply with `supabase migration up` (or `supabase db reset` locally). Then remove the dual-write code from app paths — `email_plaintext` is gone; only `email` (encrypted) is written now, through the wrapper. +The CLI emits a Supabase migration file with the drop. **Which column it drops depends on the EQL version**, which the CLI auto-detects: + +- **v3** — drops the original plaintext column, `ALTER TABLE users DROP COLUMN email;`. There was no rename, so no `email_plaintext` exists. Requires the `backfilled` phase plus a live coverage check. +- **v2** — drops the post-rename leftover, `ALTER TABLE users DROP COLUMN email_plaintext;`. Requires the `cut-over` phase. + +Review and apply with `supabase migration up` (or `supabase db reset` locally). Then remove the dual-write code from app paths — the plaintext column is gone; only the encrypted column is written now, through the wrapper. ### Inspecting progress at any time @@ -864,9 +835,8 @@ supabaseClient, encryptionClient })` factory, which takes a hand-written client-side schema and a two-argument `from(tableName, schema)`. That surface still ships in `@cipherstash/stack-supabase` and is unchanged — keep using it for existing v2 deployments — but it is not the recommended path for new -projects: use `encryptedSupabaseV3`. One current exception: the v2 wrapper is -also the interim dual-write path for CLI-managed `stash encrypt backfill` / -`stash encrypt cutover` until [cipherstash/stack#648](https://github.com/cipherstash/stack/issues/648) -lands — see "Interim path until #648: the v2 encrypted twin" in the migration -section above for a minimal recipe. For the v2 wrapper's full API and -semantics, see the docs at https://cipherstash.com/docs. +projects: use `encryptedSupabaseV3`. The CLI rollout tooling (`stash encrypt +backfill` / `cutover` / `drop`) supports both generations and auto-detects which +one a column uses, so a v2 twin is no longer needed to get CLI-managed +backfill — see the EQL version note in the migration section above. For the v2 +wrapper's full API and semantics, see the docs at https://cipherstash.com/docs. From e2d76b70cdf645fa72d0fb7a54e099d70315ce52 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Tue, 21 Jul 2026 11:35:13 +1000 Subject: [PATCH 2/2] fix(skills): qualify v3 cutover exit-code claim (exit 1 before backfill) --- skills/stash-drizzle/SKILL.md | 4 ++-- skills/stash-supabase/SKILL.md | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/skills/stash-drizzle/SKILL.md b/skills/stash-drizzle/SKILL.md index 7bcacd872..188a949c5 100644 --- a/skills/stash-drizzle/SKILL.md +++ b/skills/stash-drizzle/SKILL.md @@ -367,7 +367,7 @@ The hard case: a Drizzle table that already exists in production with live data CipherStash splits this into two named steps with a hard production-deploy gate between them: an **encryption rollout** (schema-add + dual-write code) and a **cutover step** (backfill + switch reads + drop — under EQL v2 the switch is a rename, under v3 it is an application-side change). (If using CipherStash Proxy, the rollout also includes `stash db push` to register the encryption config with EQL.) The `stash-encryption` skill is the canonical reference for the lifecycle; this section walks the Drizzle-specific shape. -> **EQL version note.** The CLI rollout tooling (`stash encrypt *`, and the underlying `@cipherstash/migrate`) works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what the schema below uses) is `schema-add → dual-write → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. +> **EQL version note.** The CLI rollout tooling (`stash encrypt *`, and the underlying `@cipherstash/migrate`) works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what the schema below uses) is `schema-add → dual-write → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a **backfilled** v3 column reports "not applicable" and exits 0 (it exits 1 if the backfill hasn't finished). > **Where am I?** Run `stash status` first (substitute the runner per the note above). It shows you which Drizzle tables/columns are mid-rollout, which are post-deploy, and what the next move is. Re-run after every transition. @@ -492,7 +492,7 @@ If something goes wrong (e.g. you discover the dual-write code wasn't actually l its own name — you switch the application to it by name, verify reads, then drop the plaintext column. Point your queries at `email_encrypted`, deploy, and confirm reads decrypt correctly; then skip ahead to the drop step. Running -`stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. +`stash encrypt cutover` on a **backfilled** v3 column reports "not applicable" and exits 0 (it exits 1 if the backfill hasn't finished). The rest of this subsection is the **EQL v2** path (a `eql_v2_encrypted` twin), kept for existing v2 deployments. diff --git a/skills/stash-supabase/SKILL.md b/skills/stash-supabase/SKILL.md index c29614942..5ba18ac41 100644 --- a/skills/stash-supabase/SKILL.md +++ b/skills/stash-supabase/SKILL.md @@ -627,7 +627,7 @@ The hard case: a Supabase table that already exists with live data in a plaintex CipherStash splits this into two named steps with a hard production-deploy gate between them: an **encryption rollout** (schema-add + dual-write code) and an **encryption cutover** (backfill + rename + drop). The `stash-encryption` skill is the canonical reference for the lifecycle; this section walks the Supabase-specific shape. -> **EQL version note.** The `stash encrypt *` tooling works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what this section's schema uses) is `rollout → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a v3 column reports "not applicable" and exits 0. +> **EQL version note.** The `stash encrypt *` tooling works with **both EQL versions** and auto-detects a column's version from its Postgres domain type — there is no flag. The lifecycles differ at the end: **v3** (the default, and what this section's schema uses) is `rollout → deploy gate → backfill → switch the app to the encrypted column by name → drop`, with **no cut-over rename**; **v2** finishes with `stash encrypt cutover` (a rename swap plus an `eql_v2_configuration` promotion) before the drop. Running `stash encrypt cutover` on a **backfilled** v3 column reports "not applicable" and exits 0 (it exits 1 if the backfill hasn't finished). > **Using CipherStash Proxy?** If you query encrypted data through [CipherStash Proxy](https://github.com/cipherstash/proxy) instead of the SDK, also run `stash db push` after schema-add and again before cutover to register the encrypted column shape with EQL. @@ -751,8 +751,9 @@ If something goes wrong (e.g. you discover the dual-write code wasn't actually l **EQL v3 (the schema above): there is no cut-over.** The encrypted column keeps its own name — point your application at `email_encrypted` through the `encryptedSupabaseV3` wrapper, deploy, verify reads decrypt correctly, then skip -ahead to the drop step. Running `stash encrypt cutover` on a v3 column reports -"not applicable" and exits 0. +ahead to the drop step. Running `stash encrypt cutover` on a **backfilled** v3 +column reports "not applicable" and exits 0 (it exits 1 if the backfill hasn't +finished). The rest of this subsection is the **EQL v2** path (an `eql_v2_encrypted` twin queried through the legacy `encryptedSupabase` wrapper), kept for existing v2