Agent skill

Stash Edge

by cipherstash in cipherstash/stack

Run CipherStash encryption on edge and non-Node runtimes with the @cipherstash/stack/wasm-inline entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun.

MITAuto-check: notesDatabases

Install Stash Edge

skills CLI
$ npx skills add cipherstash/stack --skill stash-edge -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install cipherstash/stack stash-edge --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/stash-edge .claude/skills/stash-edge && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
stash-edge
GitHub stars
157
Token cost
~6.4k tokens
SKILL.md length
2,796 words
Files
1
Skills in repo
14
Repo updated
First seen
Licence
MIT

At a glance

Run CipherStash encryption on edge and non-Node runtimes with the @cipherstash/stack/wasm-inline entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun.

  • Works in 2 steps: Authenticate as the user — build an… → Bind the data key to a claim — chain…
  • Adding encryption to a Supabase Edge Function
  • SKILL.md covers When to Use This Skill, Choosing the Entry, Importing It and Credentials, plus 6 more sections
  • Calls npm, supabase and wrangler; needs CS_CLIENT_KEY and CS_CLIENT_ACCESS_KEY

What it does

Stash Edge is an agent skill from cipherstash/stack. Run CipherStash encryption on edge and non-Node runtimes with the @cipherstash/stack/wasm-inline entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun. Covers the import specifier per runtime, which CS variables are mandatory and minting them with stash env, how keysets and credentials interact on the edge (what must match is the keyset — stash-zerokms is canonical), how the WASM client surface differs from the native typed client, why the entry is server-side only and never belongs in a browser…

Its SKILL.md is about 6.4k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Databases, covering Backend development. It works with Supabase, Deno, WebAssembly and Cloudflare Workers. The repository describes itself as: Searchable, application-level encryption for building privacy-first apps. The licence is MIT.

When your agent uses it

  • Adding encryption to a Supabase Edge Function
  • A native module fails to load in a deployed runtime
  • Wiring CS secrets into an edge deploy
  • Encrypted search returns zero rows on the edge but works locally

Example prompts

  • “/stash-edge”

Requirements

  • Node.js
  • A credential in CS_CLIENT_ACCESS_KEY
  • A credential in CS_CLIENT_KEY

Workflow steps

2 steps, taken from the first numbered list in SKILL.md.

  1. Authenticate as the user — build an OidcFederationStrategy (or
  2. Bind the data key to a claim — chain .withLockContext({ identityClaim })

What it can do on your machine

Read from SKILL.md and the folder at commit 3eb459b. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • npm
    • supabase
    • wrangler
    • vercel

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • cipherstash.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • CS_CLIENT_KEY
    • CS_CLIENT_ACCESS_KEY

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Stash Edge loads about 6.4k tokens when it runs. Until then it costs about 218 tokens; SKILL.md has 2,796 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~218
When it runs · the whole SKILL.md, loaded when a task matches
~6.4k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NoteMentions a .env fileSKILL.md:163
    nv --name my-app-prod --write    # write .env.production.local (mode 0600)
  • NoteMentions a .env fileSKILL.md:164
    stash env --name edge-dev --write .env.local
  • NoteMentions a .env fileSKILL.md:183
    supabase functions serve --env-file .env.local my-function
  • NoteMentions a .env fileSKILL.md:186
    stash env --name my-app-prod --write .env.production.local
  • NoteMentions a .env fileSKILL.md:187
    supabase secrets set --env-file .env.production.local

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from cipherstash/stack at commit 3eb459b, republished under its MIT licence (© cipherstash). 2,796 words, ~6,372 tokens.

Download SKILL.mdSave it as .claude/skills/stash-edge/SKILL.md (or your agent's skills folder).
name
stash-edge
description
Run CipherStash encryption on edge and non-Node runtimes with the `@cipherstash/stack/wasm-inline` entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun. Covers the import specifier per runtime, which `CS_*` variables are mandatory and minting them with `stash env`, how keysets and credentials interact on the edge (what must match is the keyset — `stash-zerokms` is canonical), how the WASM client surface differs from the native typed client, why the entry is server-side only and never belongs in a browser bundle, and how one EQL v3 schema module is shared across both entries. Use when adding encryption to a Supabase Edge Function, a Worker, or a Deno service; when a native module fails to load in a deployed runtime; when wiring `CS_*` secrets into an edge deploy; or when encrypted search returns zero rows on the edge but works locally.

Encryption on the Edge (WASM entry)

@cipherstash/stack has two runtime entries. The default one binds a Node-API native module and must be loaded by Node's own require. @cipherstash/stack/wasm-inline is the entry for everywhere else — it carries the WASM build of the same engine as a base64 blob inside the JS, so there is no native binding, no separate .wasm fetch, and nothing for a bundler to externalise.

This skill covers that entry and the deployment shape around it. It is EQL v3 throughout. For the SQL that actually queries the encrypted columns — the predicate forms and driver binding rules — see stash-postgres; edge functions almost always talk to Postgres over a raw driver, so the two are usually read together.

When to Use This Skill

  • Adding encryption to a Supabase Edge Function, Cloudflare Worker, Deno service, or Bun app.
  • A deployed runtime fails to load the native module (protect-ffi), or a bundler chokes trying to include it.
  • Wiring CS_* credentials into an edge deploy, or minting them at all.
  • Encrypted search works locally but returns zero rows in the deployed function — see Keysets and Credentials.
  • A schema module shared with Node tooling fails to typecheck against the edge client.

Choosing the Entry

RuntimeEntryWhy
Node server, Next.js server code@cipherstash/stack (+ /v3)Native NAPI is faster; the native module must be excluded from bundling — see the bundling guide
Supabase Edge Functions@cipherstash/stack/wasm-inlineDeno, V8-only, no native modules
Cloudflare Workers@cipherstash/stack/wasm-inlineV8 isolate, no native modules
Deno (any)@cipherstash/stack/wasm-inlineNo NAPI under Deno's default permissions
Bun@cipherstash/stack/wasm-inlineWorks, and avoids native-module resolution differences
Anywhere bundling server code@cipherstash/stack/wasm-inlineBundles cleanly; nothing to externalise

The Supabase adapter has its own edge entry. If you are using @cipherstash/stack-supabase, import @cipherstash/stack-supabase/wasm-inline (not the package root, which pulls the native engine) and declare your schemas — the adapter's default behaviour is to introspect the database for its column config, which needs a Postgres connection. Declaring skips it. Those schemas can be authored from either @cipherstash/stack/eql/v3 or @cipherstash/stack/wasm-inline — both entries resolve one declaration of the column classes, so the tables are interchangeable. See "Schema Modules Cross Entries" below, plus stash-supabase and stash-managed-platforms.

@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 that need it must be ESM ("type": "module" or .mjs).

Importing It

Supabase Edge Functions / Deno — npm: specifier

The Edge runtime resolves npm: specifiers at function start; there is no build step.

ts
import {
  Encryption, encryptedTable, types, isEncrypted,
} from 'npm:@cipherstash/stack@1.2.1/wasm-inline'

Pin an exact version. Deno caches by specifier, so an unpinned import drifts between deploys — pin, and bump the pin deliberately. Check what is current with npm view @cipherstash/stack dist-tags.

Deno with an import map

For a project with a deno.json, map the specifier once and import the bare name everywhere:

jsonc
{
  "imports": {
    "@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@1.2.1/wasm-inline"
  }
}
ts
import { Encryption, encryptedTable, types } from '@cipherstash/stack/wasm-inline'

No --allow-ffi needed. The whole point of this entry is that nothing native loads. If a Deno process running this entry ever demands an FFI permission, something has resolved the native entry instead — check the import path before granting anything.

Cloudflare Workers / Bun / bundlers — normal install
bash
npm install @cipherstash/stack
ts
import { Encryption, encryptedTable, types } from '@cipherstash/stack/wasm-inline'

No externals, no nodeExternals, no serverExternalPackages entry. If a build config already externalises the native module for the default entry, that config does not apply here and can be left alone.

Credentials

The edge client is passed its credentials explicitly. There is no credential discovery: ~/.cipherstash does not exist in a Worker or an Edge Function container, and there is no device-code login to fall back on.

clientId and clientKey are always required. Past those, config is a union: the access-key path below adds workspaceCrn + accessKey — the four CS_* values stash env mints — or you pass a pre-built config.authStrategy, which already carries the CRN and so needs neither workspaceCrn nor accessKey (see config.authStrategy below).

[!IMPORTANT] Server-side only — this entry never goes in a browser bundle. clientKey is a workspace secret, and it is required on every auth path, including authStrategy (OIDC federation): the core loads it as encryption key material before it ever calls the strategy, so per-user federation does not stand in for it. That is why there is no browser export condition, and there will not be one until the core changes (#804). Every runtime this entry targets is a server — Deno, a Worker, Bun — not a page.

ts
const client = await Encryption({
  schemas: [users],
  config: {
    workspaceCrn: Deno.env.get('CS_WORKSPACE_CRN')!,
    accessKey:    Deno.env.get('CS_CLIENT_ACCESS_KEY')!,
    clientId:     Deno.env.get('CS_CLIENT_ID')!,
    clientKey:    Deno.env.get('CS_CLIENT_KEY')!,
  },
})

Read them from the platform's environment accessor — Deno.env.get(...) on Deno/Supabase, the env binding argument on Workers, process.env on Bun.

Minting them: stash env
bash
stash env --name my-app-prod            # print the four vars to stdout
stash env --name my-app-prod --write    # write .env.production.local (mode 0600)
stash env --name edge-dev --write .env.local

This creates a fresh ZeroKMS client and a CipherStash access key from your local stash auth login session. Things that matter here:

  • The access key is shown exactly once. Pipe it straight into the secret store; it cannot be re-revealed.
  • Stdout is pipe-clean — only the dotenv block goes to stdout, so stash env --name x > prod.env and pipes into secret-store CLIs are safe.
  • Each run mints a new credential, and duplicate names are rejected. Use a distinct --name per environment.
  • CS_CLIENT_KEY and CS_CLIENT_ACCESS_KEY are secrets. Never commit them; put placeholder names in .env.example instead.
Getting them into the runtime
bash
# Supabase — local
supabase functions serve --env-file .env.local my-function

# Supabase — deployed
stash env --name my-app-prod --write .env.production.local
supabase secrets set --env-file .env.production.local

# Cloudflare Workers
wrangler secret put CS_CLIENT_KEY      # repeat per variable

# Vercel / other platforms
vercel env add CS_CLIENT_KEY production

Keysets and Credentials (when search returns zero rows)

An earlier version of this section described a "credential-identity rule": index terms deriving from the ZeroKMS client key, so rows written under one credential would decrypt but silently never match a query. That model is wrong. The scoping unit is the keyset, and stash-zerokms is the canonical skill for it. What actually holds:

  • Search terms are produced with a per-keyset index key. Every client bound to the keyset derives the same index key, so rows written by one credential match queries from another — different CS_CLIENT_ID / CS_CLIENT_KEY pairs interoperate fully as long as both clients resolve to the same keyset.
  • The routing is asymmetric (stash-zerokms has the full model): encrypt and query always use the client's bound keyset — unreachable (no grant, revoked, disabled) means client construction fails loudly at the index-key load — while decrypt follows each payload's keyset, subject to grants, with an ungranted payload failing loudly (ZeroKMS 404).
  • The old cautionary scenario — stash encrypt backfill from a laptop, then querying from an Edge Function with stash env-minted values — is fine when both clients resolve to the same keyset (the common case: both created against the workspace default). If they resolve to different keysets, how it fails depends on grants: no grant across them and decrypt fails loudly too; but a reader granted the writer's keyset while bound to its own decrypts fine and silently searches the wrong keyspace — zero rows, no error. Watch the keyset-less nuance from stash-zerokms: an operation with no explicit keyset resolves to that client's default keyset, so two clients created against different keysets don't share a keyspace even in the same workspace.

If decrypt works but a query returns zero rows, it is never the credential strings. Check, in order: the reader's bound keyset against the writer's (stash-zerokms), the operand cast / predicate form (stash-postgres), and that the extractor index exists and is used (stash-indexing).

Environment hygiene still matters, for the reasons stash-auth and stash-deployment give: mint one credential set per environment with stash env, and don't point laptop profile credentials at production data.

The Client Surface

Encryption from the WASM entry. Both entries name the factory Encryption, so the import path is the only thing that distinguishes them — which makes a stray import { Encryption } from '@cipherstash/stack' easy to miss and confusing to debug, because the two clients take different config and different bulk shapes. Check the specifier first whenever an edge client behaves unexpectedly.

Every fallible method returns the same { data } | { failure } Result contract as the native client; unwrap before use.

ts
const enc = await client.encrypt('alice@example.com', { table: users, column: users.email })
if (enc.failure) throw new Error(enc.failure.message)

const dec = await client.decrypt(enc.data)
if (dec.failure) throw new Error(dec.failure.message)

Available: encrypt, decrypt, isEncrypted, encryptQuery, encryptQueryBulk, bulkEncrypt, bulkDecrypt, encryptModel, decryptModel, bulkEncryptModels, bulkDecryptModels.

How it differs from the native typed client
Native (@cipherstash/stack)WASM (@cipherstash/stack/wasm-inline)
FactoryEncryption({ schemas })Encryption({ schemas, config }) — same name, different module
Schema authoringencryptedTable / types from @cipherstash/stack/v3the entry's own re-exports — interchangeable with the native ones (see below)
Configdiscovered from env / ~/.cipherstashpassed explicitly — clientId + clientKey, then either workspaceCrn + accessKey or a pre-built authStrategy (see below)
Typingsignatures derived from the schemaschema-aware, but not the full typed client
.audit()chainable on operationsnot available
.withLockContext()chainable on operationsnot available — see below
bulkEncrypt shape(plaintexts, { table, column }), { id, plaintext } envelopesper-item { plaintext, table, column }, plain index-aligned array
decryptModel / bulkDecryptModels(model, table, lockContext?), plus a table-less (model) overload for legacy rows(model, table) only — the table is required
Module formatESM + CJSESM only

Authentication and key binding are two different things, and conflating them is the standard mistake. Identity-bound encryption needs both:

  1. Authenticate as the user — build an OidcFederationStrategy (or AccessKeyStrategy for service-to-service) and pass it as config.authStrategy. The client then acts, on each operation, as the user whose JWT getJwt returns (stash-auth skill, "Client lifetime"). Available on this entry, and shown below.
  2. Bind the data key to a claim — chain .withLockContext({ identityClaim }) on the operation. This is what binds key retrieval to the user's claim. Not available on this entry (#797).

[!IMPORTANT] An auth strategy alone does not produce identity-bound data. It decides who the client is; a lock context decides who can retrieve a value's data key (the claim from the encrypting caller's service token is bound to the key — stash-auth is canonical). Only the first exists here, so on this entry today:

  • Values you write carry no identity condition on key retrieval — any client with keyset access can decrypt them — even with a per-user authStrategy.
  • You cannot read anything the native entry wrote under a lock context, because key retrieval requires the same claim. That is a silent split in what the two entries can read, on top of the schema incompatibility below.

If a value must be bound to an end-user claim, encrypt and decrypt it on the native entry. Don't reach for as any to force a lock context through here — there is nothing on the other side to receive it.

The strategy replaced the old per-operation token ceremony (LockContext.identify(), deprecated) — it did not replace the lock context, and the native entry still chains .withLockContext() for that.

ts
import { Encryption, OidcFederationStrategy } from '@cipherstash/stack/wasm-inline'

// `create` returns a Result — unwrap it. Passing the Result itself as
// `authStrategy` is the easy mistake, and it fails opaquely later.
const strategy = OidcFederationStrategy.create(
  workspaceCrn,                   // 'crn:<region>:<workspace-id>'
  () => getUserJwt(req),          // the current request's user; runs on every operation
)
if (strategy.failure) throw new Error(strategy.failure.error.message)

const client = await Encryption({
  schemas: [users],
  config: { authStrategy: strategy.data, clientId, clientKey },
})

// Authenticated as the end user — but the value carries no identity condition
// on key retrieval. There is no `.withLockContext()` on this entry to bind it.
const enc = await client.encrypt('alice@example.com', {
  table: users,
  column: users.email,
})
if (enc.failure) throw new Error(enc.failure.message)

On the native entry, where lock contexts do exist, the same claim must be supplied on decrypt. A value encrypted under a lock context and decrypted without one — or under a different claim — does not come back. This is the single most common identity-aware encryption bug, and it does not surface as a key error; it surfaces as a failed decrypt. It is also why an edge function cannot read what a lock-context-using Node service wrote.

AccessKeyStrategy.create(workspaceCrn, accessKey) has the same Result-returning shape, for service-to-service use with a custom token store. When you pass an auth strategy, do not also pass config.accessKey — they are mutually exclusive and the client rejects the combination.

A client built with a user-scoped strategy acts as whichever user getJwt names on each operation; constructing it per request, with req captured in getJwt as above, is the simplest way to make sure that is the current one (stash-auth skill, "Client lifetime").

Show full SKILL.md (893 more words)Show less
The bulk shape differs — don't copy the native form
ts
// WASM entry: each entry carries its own table and column.
const out = await client.bulkEncrypt([
  { plaintext: 'a@example.com', table: users, column: users.email },
  { plaintext: 'b@example.com', table: users, column: users.email },
])
if (out.failure) throw new Error(out.failure.message)
// out.data is index-aligned; a null/undefined plaintext yields null at that index.

The model helpers (encryptModel / decryptModel and their bulk forms) are present on this entry, and the traversal is the same one the native entry runs — declared columns encrypted by JS property name, everything else passing through, one ZeroKMS round trip per call.

The decrypt side has no table-less form. Both entries take the table as the second argument, and on both that is the form to prefer. What the native client also offers is a one-arg decryptModel(model) overload — the read path for rows whose table isn't in the schema set, legacy EQL v2 above all, at the cost of Date reconstruction and a precise plaintext shape. This entry has no such overload: decryptModel(model, table) and bulkDecryptModels(models, table) require the table, because they resolve date fields from a per-table map built at client construction. Omitting it throws rather than returning a { failure }, and a table the client was not initialized with is a defined failure:

ts
const rows = await client.bulkDecryptModels(encryptedRows, users)
if (rows.failure) throw new Error(rows.failure.message)

A wrapper written against the native signature will therefore compile against one entry and break on the other. This is a client-surface difference — the schema module itself is shareable (see below); wrapper code is not.

Schema Modules Cross Entries

One schema module serves both entries. A table authored with encryptedTable/types from @cipherstash/stack/v3 builds the WASM entry's Encryption, and one authored from @cipherstash/stack/wasm-inline builds the native Encryption — same types, same runtime, both directions:

ts
// schema.ts — the single source of truth for this project's schema
import { encryptedTable, types } from '@cipherstash/stack/wasm-inline'

export const users = encryptedTable('users', {
  email: types.TextSearch('email'),
  ssn:   types.TextEq('ssn'),
})

Author it against whichever entry the schema module's own runtime needs — the wasm-inline entry if that module is itself imported by the Edge Function — and pass the result to either client.

On older @cipherstash/stack versions this did not typecheck. The two entries shipped separately-emitted declarations of the column classes, and those classes carry private fields, which TypeScript compares nominally — so each entry rejected the other's schema in both directions:

text
Type 'EncryptedTextSearchColumn' is not assignable to type 'AnyEncryptedV3Column'.
  Types have separate declarations of a private property 'columnName'.

The runtime was never affected, which was the trap: as never / as any on the schema looked like the fix while silencing a signal that would matter after a genuine schema mismatch. If you see this diagnostic, upgrade rather than assert — or, on a version you cannot move off, author the schema module against exactly one entry and build only that entry's client from it.

What still does not cross is the client surface: the two entries' clients differ in the ways listed above (the bulkDecryptModels signature, config shape). A helper written against one client's signatures will not compile against the other, so keep wrapper code entry-specific even though the schema is shared.

@cipherstash/stack-supabase/wasm-inline

The Supabase adapter's edge entry runs the WASM engine but types its schemas option from @cipherstash/stack/eql/v3. That used to make it a special case — authoring the schema from @cipherstash/stack/wasm-inline was rejected there, reported one level up as schemas not assignable to AnyV3Table. It is no longer: every entry now resolves one declaration of the column classes, so either import works and both examples below are correct.

ts
// Both of these compile. The engine is WASM either way.
import { encryptedSupabase } from '@cipherstash/stack-supabase/wasm-inline'
import { encryptedTable, types } from '@cipherstash/stack/eql/v3'
ts
// The same, authored from the edge entry — useful when this module is also
// imported by the Edge Function, so one table definition serves both sides.
import { encryptedSupabase } from '@cipherstash/stack-supabase/wasm-inline'
import { encryptedTable, types } from '@cipherstash/stack/wasm-inline'

On a version predating that fix the old rule still applies: author from eql/v3 for this adapter. stash-supabase and stash-managed-platforms carry the full edge call shape.

Querying from the Edge

Edge functions rarely have an ORM, so encrypted search is usually hand-written SQL over pg or postgres-js. Mint the search needle with encryptQuery, then bind it as a typed parameter:

ts
const term = await client.encryptQuery('alice@example.com', {
  table: users, column: users.email, queryType: 'equality',
})
if (term.failure) throw new Error(term.failure.message)

// postgres-js — bind the unwrapped term, not the Result
const rows = await sql`
  SELECT * FROM users
   WHERE email = ${sql.json(term.data)}::jsonb::eql_v3.query_text_eq`

The predicate forms, the per-driver binding rules, and the query-domain names are the subject of stash-postgres — read it before writing the first query. The two rules that bite immediately: the operand must be cast to the column's eql_v3.query_* domain, and on postgres-js payloads must be bound with sql.json(...), never pre-stringified.

Troubleshooting

Dynamic require of "..." is not supported / a native .node file in the bundle — the native entry got imported. Check every import path resolves to @cipherstash/stack/wasm-inline, including transitive ones from your own shared modules.

require(...) is not a function / the specifier won't resolve in CJS — this entry is ESM-only. Move the consumer to ESM.

Missing CS_* at runtime — the secret store was never populated, or the function was served without --env-file. Validate the ones you pass at handler entry and return an actionable error rather than letting client construction fail opaquely; the example in languages/typescript/examples/supabase-worker does exactly this.

Encryption works, search returns zero rows — not a credential problem: a keyset mismatch fails everything loudly, decrypt included (see Keysets and Credentials and stash-zerokms). Empty results with working decrypt point at an untyped operand or wrong predicate form (stash-postgres); a missing index (see stash-indexing) makes queries slow, not empty.

Search needle rejected — free-text needles must be at least 3 characters; shorter ones tokenize to nothing.

Cold-start latency — the inlined WASM module is compiled on first use. Construct the client at module scope when the auth strategy is not user-scoped, so it is reused across invocations on a warm isolate.

Reference

  • stash-zerokms — keysets, clients, grants, and the key hierarchy (canonical).
  • stash-auth — credentials, auth strategies, and lock context (canonical).
  • stash-postgres — the raw-SQL predicate cookbook and driver binding rules.
  • stash-encryption — schema authoring, the types.* domain catalog, and the rollout/cutover lifecycle.
  • stash-cli — stash env, stash eql install, stash encrypt backfill.
  • stash-indexing — indexes on encrypted columns (the DDL is the same wherever the app runs).
  • stash-supabase — the PostgREST wrapper, for Supabase apps that are not writing raw SQL.
  • Working example: languages/typescript/examples/supabase-worker in the cipherstash/stack repo.
  • Bundling guide: https://cipherstash.com/docs/stack/deploy/bundling

© cipherstash, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/stash-edge of cipherstash/stack.

Open the folder on GitHubat commit 3eb459b

Compare with similar skills

Stash Edge next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Stash Edge compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Stash Edge this skillcipherstash/stack157—~6.4kAutomated safety check: NotesMIT
Add Backendahpxex/open-dashboard146—~3.6kAutomated safety check: PassMIT
Prisma 8 Contract-First ORMprisma/orm48k—~3.8kAutomated safety check: NotesApache-2.0
Database Migrationsaffaan-m/ECC275k4 repos~3kAutomated safety check: PassMIT
Database Migrationsaffaan-m/ECC275k3 repos~1.9kAutomated safety check: PassMIT
Database Migrationsaffaan-m/ECC275k1 repos~2.4kAutomated safety check: PassMIT

Similar skills

  • Add Backend

    ahpxex/open-dashboard

    Everything about the data layer — pick one of six ready-to-run backend templates (TanStack Start + Drizzle + better-auth, Hono + Drizzle + better-auth, Hono + Prisma + better-auth, Hono + Drizzle +…

    146 GitHub stars~3.6k tokensUpdated 3 mo ago
    DatabasesAuto-check passed
  • Official

    Routes Prisma 8 tasks such as contracts, migrations, queries and upgrades to the right reference files for projects on the contract-first @prisma/orm packages.

    48k GitHub stars~3.8k tokensUpdated today
    DatabasesAuto-check: notes
  • Safe, reversible database migration patterns: forward-only production changes, expand-contract zero-downtime renames, concurrent indexes, batched backfills, and per-tool workflows for PostgreSQL…

    275k GitHub starsUsed in 4 repos~3k tokens
    DatabasesAuto-check passed
  • 数据库迁移最佳实践,涵盖模式变更、数据迁移、回滚以及零停机部署,适用于PostgreSQL、MySQL及常用ORM(Prisma、Drizzle、Django、TypeORM、golang-migrate)。

    275k GitHub starsUsed in 3 repos~1.9k tokens
    DatabasesAuto-check passed
  • Şema değişiklikleri, veri migration'ları, rollback'ler ve PostgreSQL, MySQL ve yaygın ORM'ler (Prisma, Drizzle, Django, TypeORM, golang-migrate) arasında sıfır kesinti deployment'ları için…

    275k GitHub starsUsed in 1 repo~2.4k tokens
    DatabasesAuto-check passed
  • Inspect an application repository connected to PlanetScale and recommend SQLCommenter-compatible query tagging packages and conventions.

    133 GitHub stars~1.2k tokensUpdated yesterday
    DatabasesAuto-check passed

More from cipherstash/stack

All 14 skills in this repo
  • Meta Issue Creation

    cipherstash/stack

    How an agent files a GitHub issue on cipherstash repos — required structure (Background / Problem / Proposal), dumbed-down wording rules, and pre-filing checks.

    157 GitHub stars~1k tokensUpdated today
    Auto-check passed
  • Meta PR Creation

    cipherstash/stack

    How an agent authors branches, commits, and pull requests on cipherstash/stack — naming, signed commits, the changeset/skills/meta-file checklist, and PR body structure with dumbed-down wording.

    157 GitHub stars~937 tokensUpdated today
    Auto-check passed
  • Stash Zerokms

    cipherstash/stack

    The ZeroKMS key model — keysets, clients, client keys, and the grant/revoke lifecycle.

    157 GitHub stars~4.4k tokensUpdated today
    Auto-check passed
  • Stash Deployment

    cipherstash/stack

    Deploy a CipherStash encryption rollout to a live environment without losing data — the multi-deploy ladder (schema-add + dual-write → backfill → read cutover → stop dual-writes → drop plaintext)…

    157 GitHub stars~6.5k tokensUpdated today
    Auto-check passed
  • Supply-chain security controls for the @cipherstash/stack monorepo.

    157 GitHub stars~5.2k tokensUpdated today
    Auto-check: warnings
  • Stash Drizzle

    cipherstash/stack

    Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle (EQL v3).

    157 GitHub stars~10k tokensUpdated today
    Auto-check: notes

Questions about Stash Edge

What does Stash Edge do?

Run CipherStash encryption on edge and non-Node runtimes with the @cipherstash/stack/wasm-inline entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun. Stash Edge is an agent skill from cipherstash/stack. Run CipherStash encryption on edge and non-Node runtimes with the @cipherstash/stack/wasm-inline entry — Deno, Supabase Edge Functions, Cloudflare Workers, and Bun.

When should I use Stash Edge?

Stash Edge fits situations like: adding encryption to a Supabase Edge Function; A native module fails to load in a deployed runtime; wiring CS secrets into an edge deploy; encrypted search returns zero rows on the edge but works locally.

How do I install Stash Edge in Claude Code?

Run `npx skills add cipherstash/stack --skill stash-edge -a claude-code`. Or copy the skill folder (skills/stash-edge in cipherstash/stack) into .claude/skills/stash-edge in your project. Claude Code loads it when a task matches its description.

How do I install Stash Edge in Codex?

Run `npx skills add cipherstash/stack --skill stash-edge -a codex`. Or copy the skill folder (skills/stash-edge in cipherstash/stack) into .agents/skills/stash-edge in your project. Codex loads it when a task matches its description.

Can I use Stash Edge in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cipherstash/stack --skill stash-edge -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/stash-edge, .gemini/skills/stash-edge, .github/skills/stash-edge and .opencode/skills/stash-edge in your project.

What does Stash Edge need to run?

Going by SKILL.md and its folder, Stash Edge needs the command-line tools its instructions call (npm, supabase, wrangler and vercel) and credentials named CS_CLIENT_KEY and CS_CLIENT_ACCESS_KEY. Our summary lists: Node.js; A credential in CS_CLIENT_ACCESS_KEY; A credential in CS_CLIENT_KEY.

Does Stash Edge access the network?

SKILL.md names 1 domain. As links in the text: cipherstash.com. This is read from the text; nothing was executed.

Is Stash Edge safe to install?

Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Stash Edge use?

Stash Edge is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Stash Edge use?

About 6.4k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Stash Edge?

Skills that share tags, products or a category with Stash Edge: Add Backend (ahpxex/open-dashboard, 146 stars), Prisma 8 Contract-First ORM (prisma/orm, 48k stars), Database Migrations (affaan-m/ECC, 275k stars) and Database Migrations (affaan-m/ECC, 275k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Stash Edge?

cipherstash (a GitHub organization) maintains it in cipherstash/stack, which has 157 GitHub stars. The repository holds 14 skills in this directory. The repository was last updated on October 8, 2026.

Source: cipherstash/stack on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.