Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'stash': patch
---

Correct two inaccuracies in the bundled `stash-cli` skill. The `stash init` overview said the **Supabase** flow always generates an EQL migration; it now says **local Supabase**, matching `resolveMigrationRoute` — only a project with local `supabase/` CLI scaffolding takes the migration-first route, while a hosted Supabase project with no `supabase/` directory falls through to a direct `stash eql install`. And the guidance for back-dating the Supabase install migration no longer recommends `supabase db push --include-all` unconditionally: on a remote where `stash eql install` has already run, pushing the file re-runs a bundle that opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`, dropping every index, constraint, and RLS policy on those schemas. That case is now `supabase migration repair --status applied <version>` (ledger only, no SQL) — after confirming EQL is genuinely installed on that remote with `psql "$REMOTE_DATABASE_URL" -Atc "select eql_v3.version()"`; `--include-all` stays for a remote that still needs the SQL applied.

The same correction lands in the CLI itself, and in the two other places that repeated the old advice — the `stash-supabase` skill and the CLI README. `stash eql migration --supabase` warns when the project already has EQL-referencing migrations that sort before the install it is about to write, and that warning carried the identical blanket `--include-all` advice. Since this warning only fires on projects that ran `stash eql install` directly — so the remote usually already has the bundle and is missing only the ledger row — it now names `supabase migration repair --status applied <version>` as the remedy, spells out the `DROP SCHEMA IF EXISTS eql_v3 CASCADE` hazard of pushing the file instead, and keeps `--include-all` for the remote that has not had the SQL applied.
33 changes: 33 additions & 0 deletions .changeset/supabase-eql-migration-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'stash': minor
---

Add `stash eql migration --supabase`, so an EQL v3 install survives `supabase db reset` (#613).

Supabase projects previously had only `stash eql install --supabase`, which applies the SQL directly to a running database. `supabase db reset` — the ordinary local development loop — drops that database and replays `supabase/migrations/`, so the install was wiped and the next query failed with `type "eql_v3_encrypted" does not exist`. There was no supported way to get EQL into the migrations directory.

`stash eql migration --supabase` now writes `supabase/migrations/<timestamp>_cipherstash_eql.sql`, carrying the EQL v3 bundle, the `anon` / `authenticated` / `service_role` grants, and the `cipherstash.cs_migrations` tracking schema — so one `supabase db reset` provisions everything `stash encrypt` needs. The file is timestamped at generation time, so it sorts after everything already applied and pushes without `--include-all`. A second run exits rather than adding a duplicate install; `--force` regenerates the existing one in place.

The command now warns when the migrations directory already holds EQL-referencing migrations that sort *before* the install it is about to write. A project that ran `stash eql install` directly and then added `public.eql_v3_*` columns against the live database gets an install stamped today — after those migrations — and `supabase db reset`, which replays in version order with no dependency awareness, then fails with `type "eql_v3_text_search" does not exist`. The warning names the specific files and the remedy (rename the install below the earliest of them, then reconcile each remote — see below). It fires on `--dry-run` too, and nothing is renamed automatically — the ordering of someone else's deployed history is not ours to change silently.

That warning's remote guidance now requires you to verify the remote before writing to its ledger. It splits by whether the remote already has EQL: one where it does needs only the ledger row (`supabase migration repair --status applied <version>`, which runs no SQL — pushing the file instead re-runs a bundle opening with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`), and one where it does not needs the SQL genuinely applied (`supabase db push --include-all`, the back-dated version being a gap in the middle of that history). Previously the first branch was recommended on an assumption the user was never asked to check, and it is the one operation here with no self-correcting failure: mark a version applied on a remote that never ran the SQL and EQL is permanently absent *and* permanently marked applied, so no future push installs it and the first migration referencing `eql_v3` fails with nothing pointing at the cause. The warning now prints the check first — `psql "$REMOTE_DATABASE_URL" -Atc "select eql_v3.version()"` — and says never to mark applied when it errors. It asks for `eql_v3.version()` rather than the `eql_v3` schema deliberately: that function is created by the bundle's closing statements, so it cannot resolve on an install that aborted partway, while the schema is created by its opening ones and survives one. The same correction lands in the `stash-cli` and `stash-supabase` skills and the CLI README, and a guard test now fails the build if a shipped skill recommends the ledger-only repair without that check above it.

`--force`'s follow-up guidance was wrong and is now correct. It said to re-apply with `supabase db reset` (local) **or `supabase db push` (remote)**, but a push never re-applies a rewritten migration: the Supabase CLI decides what is pending by comparing versions, never file content, so an in-place rewrite keeping its version is skipped and push reports `Remote database is up to date.` The remote recipe is now `supabase migration repair --status reverted <version>` (tracking table only — it applies no SQL) followed by `supabase db push`, with `--include-all` called out as a conditional: it is needed only when migrations sort *after* the install, which leaves the reverted version as a gap in the middle of remote history. Reverting the newest version leaves it at the tail, where a plain push applies it — and the flag applies every out-of-order migration you have, so recommending it unconditionally was itself a hazard. The warning also names the hazard it never mentioned: the EQL bundle opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`, so re-applying drops every index, constraint, and RLS policy that references `eql_v3` / `eql_v3_internal` — free on a fresh `db reset`, destructive on a populated remote.

`--out` on a bare `--supabase` now warns. The Supabase CLI's migrations directory is not configurable — `supabase db reset` and `supabase db push` read `<project>/supabase/migrations` and nothing else, `config.toml` has no key for it, and `--workdir` relocates the whole `supabase/` directory rather than this subdirectory — so an install written elsewhere is never applied, which is the original bug relocated. The flag still writes the file (a project may apply that directory through its own tooling) but names the consequence, on `--dry-run` too. `--out` alongside `--drizzle --supabase` is unaffected: there it is drizzle-kit's output directory.

`--supabase` keeps its existing meaning alongside `--drizzle` (append the role grants to the Drizzle migration); only a bare `--supabase` selects the new emitter.

`stash init --supabase` now generates that migration instead of installing directly, when the project has local `supabase/` scaffolding — a hosted project without it still installs directly. Re-running init over a project that already has an install migration reports it and moves on, rather than treating the duplicate refusal as a failed setup. Its next steps no longer tell you to run `eql install --supabase` and then `supabase db reset`, which was the exact sequence that destroyed the install.

`stash init`'s EQL summary line now distinguishes the migration it wrote from one it merely found. A re-run over an existing install migration says "EQL migration **already present**" instead of "EQL migration generated" — same apply guidance, same successful exit, but no claim about work the run did not do.

`stash init`'s EQL prompt now names the action for the route it is actually on. On the migration-first routes it asks whether to generate a migration (naming `supabase/migrations/` or your Drizzle migrations folder) rather than whether to install into your database, which described the wrong action on both. Declining is fixed the same way: the retry hint is now `stash eql migration --supabase` / `--drizzle` on those routes instead of `stash eql install`, which on Supabase would reinstate the very bug above.

`stash init` now routes on the integration flags themselves rather than on the provider's display name, so combining them works. `stash init --drizzle --supabase` is accepted — and is the natural invocation for a Drizzle project on Supabase — but init joined the matched flags into a single provider name (`drizzle-supabase`) for referrer tracking and then compared that name against `'drizzle'` and `'supabase'` everywhere it had a decision to make. Every comparison went false. A local Supabase stack answers on `127.0.0.1:54322`, so host detection reports plain Postgres and the flags are the only signal left: the run installed EQL directly instead of writing a migration — nothing in `supabase/migrations/`, no `anon` / `authenticated` / `service_role` grants — which is the #613 failure this release exists to fix, reached through a flag combination the CLI accepts. The same fall-through dropped the `supabase status` hint when resolving `DATABASE_URL` (the one lookup that finds a local stack's URL), lost the Prisma Next branch for `--prisma --supabase` — scaffolding a client Prisma Next never uses and running a duplicate EQL install that races `prisma-next migrate`'s journal — and installed no integration adapter at all, where a combined run needs both `@cipherstash/stack-drizzle` and `@cipherstash/stack-supabase`. The provider now carries the matched flags alongside its name and every step reads those; the combined name is still exactly what gets recorded as the referrer, it is simply no longer what the CLI branches on. Drizzle still wins the migration route when both flags fire — it owns the migration history, and `--supabase` is the grants modifier there. Single-flag runs behave exactly as before.

Also corrects the remote apply command across the Supabase guidance: a bare `supabase migration up` targets the local database, so the instructions now say `supabase db push`.

Also corrects the `eql install --migration` removal message, which pointed every Supabase user at `--drizzle`.

The Supabase CLI behaviour all of the above depends on is now pinned by a live test rather than by reading the CLI's source. `supabase-push.live.test.ts` drives the real binary against a real Postgres — `db push --db-url` needs neither Docker nor a linked project — and covers: the generated install applying with no `--include-all`; `anon` reaching `eql_v3` via `SET ROLE` through the grants carried in the emitted file (not just the ones `eql install --direct` applies); an out-of-order version aborting the whole push rather than being skipped; a `--force`-replaced file never re-applying; `--include-all` being needed only for the gap case; and a leaked `.tmp` file being ignored. Gated on `STASH_TEST_SUPABASE_DB_URL` + `STASH_TEST_SUPABASE_CLI`, so the default suite is unchanged.
2 changes: 1 addition & 1 deletion e2e/tests/package-managers.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('CLI init providers — package-manager-aware Next Steps', () => {
label: 'supabase',
create: createSupabaseProvider,
firstStep: (r) =>
`Install EQL: ${r} stash eql install --supabase (prompts for migration vs direct)`,
`Install EQL: ${r} stash eql migration --supabase (writes it into supabase/migrations/)`,
},
]

Expand Down
49 changes: 47 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,11 @@ Reads `databaseUrl` from `stash.config.ts`.

---

## Drizzle migration mode
## Migration mode

Use `eql migration --drizzle` to add EQL v3 installation to Drizzle migration history instead of applying it directly.
Use `eql migration` to add the EQL v3 installation to your migration history instead of applying it directly. The install then ships to every environment through the same migrate step as the rest of your schema.

### Drizzle

```bash
npx stash eql migration --drizzle
Expand All @@ -306,6 +308,49 @@ npx drizzle-kit migrate

`drizzle-kit` must be installed in your project (`npm install -D drizzle-kit`). The `--out` directory must match your `drizzle.config.ts`.

Add `--supabase` on a Supabase-hosted Drizzle project to append the `anon` / `authenticated` / `service_role` grants.

### Supabase

```bash
npx stash eql migration --supabase
supabase db reset # local
supabase db push # remote/linked project
```

This writes `supabase/migrations/<timestamp>_cipherstash_eql.sql` containing the EQL v3 bundle, the Supabase role grants, and the `cipherstash.cs_migrations` tracking schema — so one reset provisions everything `stash encrypt` needs.

**Use this rather than `eql install --supabase` whenever the project has a local `supabase/` directory.** A direct install does not survive `supabase db reset`, which drops the database and replays the migrations directory.

The file is timestamped at generation time, so it sorts after everything already applied and pushes with no extra flag. An out-of-order version is not merely skipped — `supabase db push` aborts the whole push with `Found local migration files to be inserted before the last migration on remote database.` and applies nothing until you re-run with `--include-all`.

If the project already has migrations that reference EQL (an `eql_v3_*` column added back when `eql install` was applied directly), those now sort *before* the install. `supabase db reset` replays in version order with no dependency awareness, so they run first and the reset fails with `type "eql_v3_text_search" does not exist`. The command warns and names them; rename the install migration to a version below the earliest of them so it replays first.

How that back-dated version reaches a remote depends on what that remote actually has, so check before touching the ledger:

```bash
psql "$REMOTE_DATABASE_URL" -Atc "select eql_v3.version()"
```

`eql_v3.version()` is created by the bundle's last statements, so it answers "is the whole install there" — a probe for the `eql_v3` schema does not, since that schema is created by the bundle's first statements and survives an install that aborted partway.

If it prints a version, EQL is present and only the ledger row is missing — mark it applied with `supabase migration repair --status applied <version>`, which writes the row and runs no SQL. Do not push the file there instead: that re-runs a bundle opening with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`, dropping every index, constraint, and RLS policy that references those schemas.

If it errors, that remote genuinely still needs the SQL applied: `supabase db push --include-all`. Never mark it applied there — the ledger row would claim SQL that never ran, so no later push installs EQL, and the first migration referencing `eql_v3` fails with nothing pointing at the cause.

Pass `--force` to regenerate an existing install migration in place. It keeps its version, so `supabase db push` will **not** re-apply it — pending migrations are decided by version, never by file content, and push reports `Remote database is up to date.` Use `supabase db reset` locally, or on a remote:

```bash
supabase migration repair --status reverted <version> # clear the ledger row (applies no SQL)
supabase db push # re-apply
```

Add `--include-all` to that push only if it aborts with `Found local migration files to be inserted before the last migration on remote database.` — that happens when migrations sort after the install, leaving the reverted version as a gap in the middle of history. Reverting the newest version leaves it at the tail, which a plain push applies. The flag applies every out-of-order migration you have, so don't pass it pre-emptively.

Weigh that before doing it to a populated database: the EQL bundle opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE` (and `eql_v3_internal`), so re-applying also drops every index, constraint, and RLS policy that references those schemas.

Don't pass `--out` here. The Supabase CLI reads `<project>/supabase/migrations` and nothing else — the path is not configurable in `config.toml`, and `--workdir` moves the whole `supabase/` directory, not this one. An install written elsewhere is never applied by `supabase db reset` / `db push`, which is the failure this command exists to avoid. The flag still works (and warns) for projects that apply another directory through their own tooling.

---

### `npx stash eql repair --drizzle`
Expand Down
Loading
Loading