From 1f36c068d274c614456e084f0a513a10c5b4e864 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 18 Aug 2026 22:39:22 +1000 Subject: [PATCH] skills: document the managed AI platform path (#893) Lovable, v0, Bolt and Replit share a shape -- no shell the developer controls, an edge/Workers runtime, a database role that is not `postgres`, and schema changes only through the platform's own migration tool -- and each of those changes the setup. Nothing covered it. The costly fact goes first, because it decides whether anyone gets further: use `@cipherstash/stack` with the `wasm-inline` entry. `@cipherstash/protect` is the deprecated predecessor, and reasoning from its `-ffi` dependency to "CipherStash cannot run on an edge runtime" is a wrong conclusion drawn from the wrong package -- it cost an agent a full turn on a real project before it found `stash`. The same correction now sits in `stash-edge`'s entry table, where an agent comparing runtimes will hit it. The rest: headless `auth login --json` in an ephemeral sandbox (the CLI can authenticate there, which the reporter did not know and worked around by fetching a raw .sql release asset), minting `CS_*` with `stash env`, installing EQL as a non-`postgres` role or generating a migration instead, which predicates survive PostgREST, and why `encryptedSupabase` cannot be constructed in a Worker. Two things lifted above the fold in `stash-supabase`: a pointer to the new skill, and a one-line PostgREST summary. The full treatment was correct but ~500 lines down, which is not where a time-pressured agent finds it. Registered for `supabase` and `postgresql` in both the CLI and wizard skill maps, in SKILL_PURPOSES, and in the AGENTS.md inventory and routing table. Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1 --- .changeset/skills-managed-platforms.md | 13 ++ AGENTS.md | 3 +- .../src/commands/init/lib/install-skills.ts | 9 +- .../cli/src/commands/init/lib/setup-prompt.ts | 2 + packages/wizard/src/lib/install-skills.ts | 7 +- skills/stash-edge/SKILL.md | 9 ++ skills/stash-managed-platforms/SKILL.md | 132 ++++++++++++++++++ skills/stash-supabase/SKILL.md | 4 + 8 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 .changeset/skills-managed-platforms.md create mode 100644 skills/stash-managed-platforms/SKILL.md diff --git a/.changeset/skills-managed-platforms.md b/.changeset/skills-managed-platforms.md new file mode 100644 index 000000000..9c53be49d --- /dev/null +++ b/.changeset/skills-managed-platforms.md @@ -0,0 +1,13 @@ +--- +'stash': patch +--- + +New `stash-managed-platforms` skill: implementing CipherStash on a managed AI app platform (Lovable, v0, Bolt, Replit). + +These platforms share a shape — no shell the developer controls, an edge/Workers runtime, a database role that is not `postgres`, and schema changes only through the platform's own migration tool — and every one of those changes the setup. The skill covers the WASM entry, running `stash auth login --json` headlessly in an ephemeral sandbox, minting `CS_*` with `stash env`, installing EQL as a non-`postgres` role (including generating a migration instead of installing directly), which predicates survive PostgREST, and why `encryptedSupabase` cannot be constructed inside a Worker. + +The costly one is first, because it decides whether anyone gets any further: **use `@cipherstash/stack` with the `@cipherstash/stack/wasm-inline` entry.** `@cipherstash/protect` is the deprecated predecessor, and reasoning from its `@cipherstash/protect-ffi` dependency to "there is no way to run this on an edge runtime" is a wrong conclusion drawn from the wrong package. That dead end cost an agent a full turn on a real project before it found `stash`. The same correction is now stated in `stash-edge`'s entry table, where an agent comparing runtimes will hit it. + +Two things were also lifted above the fold in `stash-supabase`: a pointer to the new skill, and a one-line summary of what does and does not survive PostgREST (`eq`/`neq`/`in`/`match()` and the range filters do; encrypted `matches()` and JSON containment do not). The full treatment was correct but ~500 lines down, which is not where a time-pressured agent finds it. + +Registered for the `supabase` and `postgresql` integrations in both the CLI and wizard skill maps, so it installs into `.claude/skills` / `.codex/skills` and inlines into `AGENTS.md` on those paths. diff --git a/AGENTS.md b/AGENTS.md index c95d1f04b..6f94d59c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,7 +86,7 @@ If these variables are missing, tests that require live encryption will fail or - `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README) - `examples/*`: Working apps (basic, prisma, supabase-worker) - `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo). -- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-deployment`, `stash-zerokms`, `stash-auth`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma`, `stash-supply-chain-security`) +- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-deployment`, `stash-zerokms`, `stash-auth`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma`, `stash-managed-platforms`, `stash-supply-chain-security`) ## Working on protect-ffi @@ -244,6 +244,7 @@ nothing type-checks them, and the damage lands in a customer's repo, not ours. | Drizzle / Supabase / Prisma Next / DynamoDB integrations | `skills/stash-drizzle`, `skills/stash-supabase`, `skills/stash-prisma`, `skills/stash-dynamodb` | | The rollout/cutover lifecycle (`packages/migrate`, `stash encrypt *`) | `skills/stash-encryption` and `skills/stash-cli` | | The deploy sequencing / deploy-gate story, `stash env`, or platform-specific deployment guidance | `skills/stash-deployment` | +| The managed AI platform path (Lovable, v0, Bolt, Replit) — headless auth, non-`postgres` roles, PostgREST limits | `skills/stash-managed-platforms` | | The `@cipherstash/eql` pin, `eql install`/`eql migration` behaviour, or index-related SQL guidance | `skills/stash-indexing` | | The EQL operator/domain surface (`eql_v3.query_*` casts, predicate forms) | `skills/stash-postgres` | | The keyset/client model (`config.keyset`, grants, the ZeroKMS access story) | `skills/stash-zerokms` — the canonical source; other skills should point here rather than restate it | diff --git a/packages/cli/src/commands/init/lib/install-skills.ts b/packages/cli/src/commands/init/lib/install-skills.ts index 8f798bed5..223dea04f 100644 --- a/packages/cli/src/commands/init/lib/install-skills.ts +++ b/packages/cli/src/commands/init/lib/install-skills.ts @@ -23,13 +23,17 @@ export const SKILL_MAP: Record = { // Supabase gets the raw-SQL and edge skills on top of its own: Edge // Functions are the flagship use of the WASM entry, and Supabase projects // write hand-written SQL in migrations and RPC even when the app itself - // goes through PostgREST (#754). + // goes through PostgREST (#754). `stash-managed-platforms` rides with them + // because the hosted AI builders are overwhelmingly Supabase-backed, and + // the failure it prevents — concluding the product cannot run here at all — + // happens before an agent knows enough to go looking for it (#893). supabase: [ 'stash-encryption', 'stash-supabase', 'stash-indexing', 'stash-postgres', 'stash-edge', + 'stash-managed-platforms', 'stash-deployment', 'stash-zerokms', 'stash-auth', @@ -47,11 +51,14 @@ export const SKILL_MAP: Record = { // The no-ORM path: `stash-postgres` (binding + predicate forms) and `stash-edge` // (WASM entry, CS_* credentials) are the two skills this integration has no // other source for — everything else assumes an ORM emits the operands. + // `stash-managed-platforms` joins them: v0, Bolt and Replit reach a plain + // Postgres this way, and hit the same four constraints (#893). postgresql: [ 'stash-encryption', 'stash-indexing', 'stash-postgres', 'stash-edge', + 'stash-managed-platforms', 'stash-deployment', 'stash-zerokms', 'stash-auth', diff --git a/packages/cli/src/commands/init/lib/setup-prompt.ts b/packages/cli/src/commands/init/lib/setup-prompt.ts index 2ef268670..6db238329 100644 --- a/packages/cli/src/commands/init/lib/setup-prompt.ts +++ b/packages/cli/src/commands/init/lib/setup-prompt.ts @@ -206,6 +206,8 @@ const SKILL_PURPOSES: Record = { 'hand-written Postgres SQL over `pg` / `postgres-js`: the encrypted predicate matrix, `eql_v3.query_*` operand casts, per-driver parameter binding', 'stash-edge': 'the `@cipherstash/stack/wasm-inline` entry for Deno / Supabase Edge Functions / Workers: imports, `CS_*` credentials, the credential-identity rule', + 'stash-managed-platforms': + 'managed AI app platforms (Lovable, v0, Bolt, Replit): the WASM entry, headless `auth login --json`, minting `CS_*` with `stash env`, installing EQL as a non-`postgres` role, and which predicates survive PostgREST', 'stash-deployment': 'taking a rollout to a live environment — the multi-deploy ladder and its gates, backfilling against a production database, `CS_*` credentials at build and run time, Prisma Postgres/Compute specifics', 'stash-zerokms': diff --git a/packages/wizard/src/lib/install-skills.ts b/packages/wizard/src/lib/install-skills.ts index a870652f2..d88615b11 100644 --- a/packages/wizard/src/lib/install-skills.ts +++ b/packages/wizard/src/lib/install-skills.ts @@ -21,14 +21,16 @@ export const SKILL_MAP: Record = { 'stash-auth', 'stash-cli', ], - // `stash-postgres` / `stash-edge` mirror the CLI's SKILL_MAP — see the comments - // there for why Supabase and the generic (no-ORM) path get them (#754). + // `stash-postgres` / `stash-edge` / `stash-managed-platforms` mirror the CLI's + // SKILL_MAP — see the comments there for why Supabase and the generic + // (no-ORM) path get them (#754, #893). supabase: [ 'stash-encryption', 'stash-supabase', 'stash-indexing', 'stash-postgres', 'stash-edge', + 'stash-managed-platforms', 'stash-deployment', 'stash-zerokms', 'stash-auth', @@ -48,6 +50,7 @@ export const SKILL_MAP: Record = { 'stash-indexing', 'stash-postgres', 'stash-edge', + 'stash-managed-platforms', 'stash-deployment', 'stash-zerokms', 'stash-auth', diff --git a/skills/stash-edge/SKILL.md b/skills/stash-edge/SKILL.md index ca68d3c7c..e6831edd1 100644 --- a/skills/stash-edge/SKILL.md +++ b/skills/stash-edge/SKILL.md @@ -41,6 +41,15 @@ together. | Bun | `@cipherstash/stack/wasm-inline` | Works, and avoids native-module resolution differences | | Anywhere bundling server code | `@cipherstash/stack/wasm-inline` | Bundles cleanly; nothing to externalise | +**`@cipherstash/protect` is not one of the options.** It is the deprecated +predecessor of `@cipherstash/stack`; its native `@cipherstash/protect-ffi` +dependency will not load in any of the runtimes above. Reasoning from *that* +package's dependency tree to "CipherStash cannot run on the edge" is a wrong +conclusion drawn from the wrong package — it has already cost one agent a +full turn on a hosted platform. The row you want is `wasm-inline`. (On a +managed AI platform specifically — Lovable, v0, Bolt, Replit — see +`stash-managed-platforms`.) + **The WASM entry is ESM-only.** Its `exports` map has an `import` condition and no `require` — deliberately, since the runtimes it targets are ESM. A CJS `require('@cipherstash/stack/wasm-inline')` will not resolve. Node consumers diff --git a/skills/stash-managed-platforms/SKILL.md b/skills/stash-managed-platforms/SKILL.md new file mode 100644 index 000000000..46d03c162 --- /dev/null +++ b/skills/stash-managed-platforms/SKILL.md @@ -0,0 +1,132 @@ +--- +name: stash-managed-platforms +description: Implement CipherStash encryption on a managed AI app platform — Lovable, v0, Bolt, Replit, and anything else with no developer-controlled shell, an edge/Workers runtime, a database role that is not `postgres`, and schema changes only through the platform's own migration tool. Covers the one fact that decides whether the product works there at all (use `@cipherstash/stack` with the `wasm-inline` entry — `@cipherstash/protect` is the deprecated predecessor and its native module will not load), running `stash auth login --json` headlessly in an ephemeral sandbox, minting deployment credentials with `stash env`, installing EQL as a role that is not `postgres`, which query predicates survive PostgREST, and why `encryptedSupabase` cannot be constructed inside a Worker. Use when the project is hosted on one of these platforms, when there is no terminal you control, when a native module fails to load in the deployed runtime, or when you are about to conclude CipherStash cannot be used here. +--- + +# CipherStash on Managed AI App Platforms + +## Read this first: it does work here + +If you are on Lovable, v0, Bolt, Replit, or a similar hosted builder, and you have found `@cipherstash/protect` and inferred from its `@cipherstash/protect-ffi` dependency that CipherStash needs a native Rust addon and therefore cannot run on an edge runtime — **that inference is about the wrong package.** + +- **`@cipherstash/stack` is the current package.** Import the `@cipherstash/stack/wasm-inline` entry. It carries the WASM build of the same engine as a base64 blob inside the JS: no native binding, no separate `.wasm` fetch, nothing for a bundler to externalise, no `--allow-ffi`. +- **`@cipherstash/protect` is the deprecated predecessor.** Its native FFI will not load in a Worker or an Edge Function. Do not install it, and do not use it to decide what the product can do. + +An agent working on one of these platforms previously spent a full turn concluding CipherStash was impossible in a Lovable project before finding `stash` and the WASM entry. Nothing about that conclusion was true. Everything below is the rest of what that turn had to discover. + +The `stash-edge` skill is the full guide to the WASM entry (per-runtime import specifiers, the client surface, why schema modules cannot be shared across entries). This page covers what is specific to *managed* platforms. + +## When to Use This Skill + +Use it when the environment has any of these four properties — they travel together and each one changes the setup: + +1. **No shell the developer controls.** Commands run in an ephemeral sandbox, or not at all. +2. **An edge / Workers runtime** for server code. +3. **A database role that is not `postgres`** and is not a member of it (Lovable's is `sandbox_exec`). +4. **Database changes only through the platform's migration tool**, not a psql session. + +Do not use it for a normal Node deployment on a VM or a container — that is `stash-deployment`. + +## The CLI authenticates headlessly. Use it. + +The most common wrong turn after the WASM one is assuming the CLI cannot log in without a browser on the machine, and reaching for a raw EQL `.sql` release asset instead. The device flow is designed for exactly this case. + +```bash +stash auth login --json --region us-east-1 +``` + +`--json` emits newline-delimited JSON on stdout, one object per line, and deliberately does **not** open a browser — the human opens the URL, not the agent's host. The first event, `authorization_required`, carries `verificationUri` / `verificationUriComplete` and a `userCode`; print it and ask the person to approve it. Credentials are written to `~/.cipherstash`, which exists fine inside a sandbox. + +Three things to get right: + +- **`--region` is required in a non-TTY.** Without it (or `STASH_REGION`) the region picker cannot render and the command exits `region_required`. `stash auth regions --json` lists the valid slugs. +- **After printing `authorization_required` the command blocks**, polling until approval or expiry (~900 s). Run it as a background task with a generous timeout — do not treat the pause as a hang. +- **Authenticate before `stash init`.** An unauthenticated `init` tries to start a login of its own. + +Then drive everything else through the CLI: `stash manifest --json` is the authoritative command surface, and `--json` modes exist on the commands an agent needs. Never `cat` anything under `~/.cipherstash` — the CLI reads it for you, and its contents are secrets. + +## Deployment credentials: `stash env` + +The four `CS_*` variables a deployed app needs are minted in one command: + +```bash +stash env --name my-app-prod --write .env.production.local +``` + +`--name` is **required** in a non-interactive run. Without `--write` the dotenv block goes to stdout and progress UI goes to stderr, so redirecting into a file or piping into a secret store is safe. With `--write` the file is mode 0600, and an existing file is *refused* non-interactively rather than silently overwritten. The access key is shown exactly once. + +Put those four values into the platform's backend-secrets UI. That is the whole story on these platforms and it works as-is — `stash-auth` is canonical for what each variable is and how the strategies use them. + +## The database role is not `postgres`, and that is fine + +`stash eql install` completes as a non-`postgres` role. Only three owner-scoped `ALTER DEFAULT PRIVILEGES FOR ROLE postgres` statements are skipped, and they are **optional**: they cover EQL objects `postgres` might later create outside stash tooling, and stash re-grants every object on each install and upgrade. The CLI prints the skipped statements under "Optional SQL — requires postgres" for an operator who wants to apply them another way. + +**Check before you install**, and pass the report to the human rather than guessing: + +```bash +stash eql preflight --json +``` + +It reports `current_user`, superuser, membership of `postgres` (never blocking), `CREATE` on the database and on `public`, `pgcrypto` presence *and placement*, whether the role can create an operator class, and whether the EQL schemas already exist. Each blocked row names the statement it blocks. Exit 1 means a genuine blocker. + +Two of its answers change what you write afterwards: + +- **`ORE operator class: not creatable`** — declare ordered columns `types.*Ord`, never `types.*OrdOre`. See the capability matrix in `stash-encryption`; the short version is that the ORE domains get an always-raising CHECK on such a database, so writes to them fail. +- **`pgcrypto` outside `extensions` / `public`** aborts the bundle for *any* role, superuser included. + +### Getting the SQL applied through the platform's migration tool + +Where you cannot hold a connection open — or where the platform replays a migrations directory and would wipe a direct install — generate a migration instead of installing: + +```bash +stash eql migration --supabase # writes into supabase/migrations/ +stash eql migration --drizzle # a Drizzle custom migration +``` + +On a Supabase-backed platform this is the **only durable** option: `supabase db reset` replays `supabase/migrations/` and discards anything a direct `eql install` did. The generated Supabase migration wraps the owner-scoped statements in a `pg_has_role` guard, so it applies cleanly whatever role the platform's migration runner uses — a non-member role skips them instead of aborting the whole migration. + +Then commit the migration through the platform's Git sync and let its own migrate step apply it. + +## What survives PostgREST + +If the app talks to the database through Supabase's Data API rather than a Postgres connection, the predicate surface is narrower than the type surface. Agents guess wrong in **both** directions on this, so take it from the table: + +| Through PostgREST | Works? | +|---|---| +| `eq`, `neq`, `in`, `match()` | ✅ — the adapter encrypts each filter value with the full storage path | +| `gt`, `gte`, `lt`, `lte` | ✅ on range-capable domains (`*_ord`, `text_search`) | +| `order()` | ✅ on OPE-backed ordering columns only (`*_ord`, `text_ord`, `text_search`) | +| `matches()` — encrypted free-text | ❌ needs `@@` with an `eql_v3.query_*` cast PostgREST cannot emit | +| encrypted `contains()` / `selectorEq()` / `selectorNe()` | ❌ needs an `eql_v3.query_json` cast, likewise | + +The wrapper fails fast on the unsupported ones rather than silently returning wrong rows. Do not reach for `like` / `ilike` / raw `cs` as substitutes — they are not equivalent and will not match encrypted data. + +When you need free-text or encrypted-JSON predicates, the query has to go through something that can emit a cast: Drizzle, Prisma Next, or hand-written SQL in an RPC (`stash-postgres` has the raw forms). `stash-supabase` is canonical for the adapter's full behaviour, including the security note about filter operands travelling in GET query strings. + +## `encryptedSupabase` cannot be constructed in a Worker + +`encryptedSupabase` introspects the database to build its schema, so it needs a **Postgres connection** — which a Worker or an Edge Function does not have (cipherstash/stack#708). Constructing it there fails; this is a property of the wrapper, not a configuration mistake to debug. + +Two supported shapes: + +- **Construct it server-side** where a Postgres connection exists, and keep the Worker to code paths that do not need it. +- **In the edge runtime, use `@cipherstash/stack/wasm-inline` directly**: encrypt and decrypt with the client, and send the resulting EQL payloads through the Supabase JS client or raw SQL yourself. `stash-edge` covers the client surface and `stash-postgres` the SQL forms. + +## Order of operations + +1. `stash auth login --json --region ` — hand the verification URL to the human. +2. `stash eql preflight --json` — report the role's capability before changing anything. +3. `stash eql migration --supabase` (or `--drizzle`), committed through the platform's Git sync — not a direct `eql install`, if the platform replays a migrations directory. +4. `stash init`, then `stash plan` / `stash impl --target ` to scaffold and hand off. On Lovable, `--target lovable` writes an `AGENTS.md` whose next-steps are platform-specific. +5. `stash env --name --write` — put the four `CS_*` values into the platform's backend secrets. +6. Write the schema with `types.*Ord`, not `*OrdOre`, unless preflight said the operator class is creatable. +7. `stash eql verify` — confirm the installed surface is complete before shipping. + +## Related skills + +- `stash-edge` — the WASM entry in depth: per-runtime imports, the client surface, credentials on the edge. +- `stash-supabase` — the `encryptedSupabase` wrapper, its filters, and the full PostgREST behaviour. +- `stash-cli` — every command, its flags, and the non-interactive escape hatches. +- `stash-auth` — canonical for `CS_*`, auth strategies, and lock context. +- `stash-encryption` — the capability matrix: which `types.*` factory supports which predicate. +- `stash-postgres` — raw SQL with the `eql_v3.query_*` operand casts, for the no-ORM fallback. diff --git a/skills/stash-supabase/SKILL.md b/skills/stash-supabase/SKILL.md index f6609cd13..9e0dfc09b 100644 --- a/skills/stash-supabase/SKILL.md +++ b/skills/stash-supabase/SKILL.md @@ -26,6 +26,10 @@ selects, with support for equality, range, and ordering. - Using identity-aware encryption (lock contexts) with Supabase - Building applications where sensitive columns need encryption at rest and in transit +> **On a managed AI platform — Lovable, v0, Bolt, Replit — read `stash-managed-platforms` first.** Two things there are decided before anything on this page applies: server code runs on an edge runtime, so it needs `@cipherstash/stack/wasm-inline` (`@cipherstash/protect` is the deprecated predecessor and its native module will not load — that dead end has cost an agent a whole turn), and the database role is not `postgres`, which changes how EQL gets installed. `encryptedSupabase` also cannot be constructed inside a Worker: it introspects the database and so needs a Postgres connection. + +> **What survives PostgREST, in one line** (the full treatment is under [Query behaviour on encrypted columns](#query-behaviour-on-encrypted-columns), a long way down): `eq` / `neq` / `in` / `match()` and the range filters `gt` / `gte` / `lt` / `lte` **do** work on capable domains, and so does `order()` on OPE-backed ordering columns. Encrypted free-text `matches()` and encrypted-JSON `contains()` / `selectorEq()` / `selectorNe()` **do not** — they need `eql_v3.query_*` casts PostgREST cannot emit, and the wrapper fails fast rather than returning wrong rows. Agents guess wrong in both directions on this, so don't infer it; for the predicates that don't survive, use Drizzle, Prisma Next, or SQL in an RPC. + ## Installation ```bash