Edge Functions
Run CipherStash encryption inside Supabase Edge Functions using the @cipherstash/stack WebAssembly build, with the CipherStash token cached across invocations in an HTTP-only cookie.
Encryption and decryption must run somewhere your keys and credentials are safe — never in the browser. Supabase Edge Functions are that place: server-side compute next to your database. CipherStash ships a WebAssembly build of the SDK for exactly this runtime, so the same encrypt/decrypt code runs in Deno with no native bindings.
Why the wasm build
Supabase Edge Functions run on Deno, which loads npm packages through the node compatibility layer. The SDK's default entry resolves to native NAPI bindings that don't exist in that runtime, so the edge uses a dedicated entry: @cipherstash/stack/wasm-inline. It embeds the WebAssembly module as base64 inside the JS bundle — no separate .wasm fetch, no bundler plugins, no static_files config. The same applies to the auth package (@cipherstash/auth/wasm-inline).
Set up the function
Map the wasm-inline entries in your function's deno.json:
{
"imports": {
"@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@^1.0.0/wasm-inline",
"@cipherstash/auth/wasm-inline": "npm:@cipherstash/auth@^0.42/wasm-inline",
"@cipherstash/auth/cookies": "npm:@cipherstash/auth@^0.42/cookies"
}
}The developer profile is not supported in Edge Functions. stash auth login creates a profile for the native Node.js SDK on your development machine. An Edge Function runs the WebAssembly client inside an isolated Deno runtime, where that profile and automatic credential discovery are unavailable.
Provide the four CS_* values explicitly through an env file when serving locally and through Supabase secrets when deployed.
Choose a credential source
Use the developer profile locally. For CI or a deployed application, create a separate credential set with the CLI or Dashboard.
Run:
npx stash auth loginThe native Stack client uses the developer profile automatically.
For an Edge Function, use the CLI or Dashboard option; the developer profile cannot cross into the Deno runtime. To create a dedicated local credential set and write it directly to the function's env file:
npx stash env --name edge-dev --write ./supabase/functions/.env.localNever commit this file. Each successful stash env run creates a new credential with a unique name, and the access key cannot be shown again.
Then serve the function against that file:
supabase functions serve --env-file ./supabase/functions/.env.localFor deployed functions, add the same four values as Edge Function secrets. Use a separate credential set for production rather than copying the local values.
Encrypt and decrypt in a function
Each invocation builds a fresh client, but the CipherStash service token is cached in an HTTP-only cookie via cookieStore, so only the first request per session pays the full round-trip to CipherStash:
import { Encryption, AccessKeyStrategy, encryptedTable, encryptedColumn } from "@cipherstash/stack/wasm-inline"
import { cookieStore } from "@cipherstash/auth/cookies"
const patients = encryptedTable("patients", {
email: encryptedColumn("email").equality(),
})
Deno.serve(async (req) => {
const responseHeaders = new Headers({ "content-type": "application/json" })
// Cache the CipherStash token across invocations in an HTTP-only cookie
const authStrategy = AccessKeyStrategy.create(
Deno.env.get("CS_WORKSPACE_CRN")!,
Deno.env.get("CS_CLIENT_ACCESS_KEY")!,
{ store: cookieStore({ request: req, responseHeaders }) },
)
if (authStrategy.failure) {
return Response.json({ error: authStrategy.failure.type }, { status: 500, headers: responseHeaders })
}
const client = await Encryption({
schemas: [patients],
config: {
authStrategy: authStrategy.data,
clientId: Deno.env.get("CS_CLIENT_ID")!,
clientKey: Deno.env.get("CS_CLIENT_KEY")!,
},
})
const encrypted = await client.encrypt("[email protected]", { column: patients.email, table: patients })
const decrypted = await client.decrypt(encrypted.data)
return Response.json({ ok: decrypted.data === "[email protected]" }, { headers: responseHeaders })
})Note the .failure / .data result shape: on the wasm entry, AccessKeyStrategy.create returns a Result you unwrap (unlike the Node build, where it returns the strategy directly).
Per-user identity on the edge
To encrypt as the signed-in Supabase user rather than a service credential, swap AccessKeyStrategy for OidcFederationStrategy — its getJwt callback returns the current Supabase session token. The mechanics and the identity-lock layer are covered in Supabase Auth; the only edge-specific difference is the wasm-inline import and the Result unwrap:
import { OidcFederationStrategy } from "@cipherstash/stack/wasm-inline"
import { cookieStore } from "@cipherstash/auth/cookies"
const authStrategy = OidcFederationStrategy.create(
Deno.env.get("CS_WORKSPACE_CRN")!,
() => getSupabaseSessionToken(req), // return the current session JWT
{ store: cookieStore({ request: req, responseHeaders }) },
)Caveats
- Cold start. The first request pays the full path: WebAssembly compile, auth round-trip to CipherStash, and ZeroKMS dataset-key initialization. Subsequent requests reuse the cookie-cached token and only pay for the encrypt/decrypt work.
- Bundle size. The inline wasm build is larger than a native binding (the module ships as base64 in the JS). That's the trade for zero runtime config — acceptable for an edge function that boots once per worker.
- Server-side only. These credentials and the session token must never reach the browser. Keep this code in the Edge Function.
Where to next
Supabase Auth
Federate the Supabase Auth session into CipherStash so encryption authenticates as the signed-in user — then, optionally, lock decryption to that user's identity.
Overview
API reference for @cipherstash/stack-supabase: every exported type, function, and class, with signatures, parameters, and usage.