Background
CipherStash Stack encrypts individual database columns. The encryption engine is written in Rust and compiled two ways: a native binary for Node.js, and a WebAssembly (WASM) build for runtimes that cannot load a native binary.
The WASM build ships through two entry points — @cipherstash/stack/wasm-inline and @cipherstash/stack-supabase/wasm-inline. "Inline" means the compiled WASM is embedded in the JavaScript file as a base64 string, so no separate .wasm file has to be fetched at runtime. This is what makes the package work on Deno, Bun, Cloudflare Workers and Supabase Edge Functions.
Vercel runs some code on an edge runtime, which is a restricted JavaScript environment rather than full Node.js. Vercel has deprecated standalone Edge Functions, but Routing Middleware still uses the edge runtime by default, and that is supported. Middleware is the middleware.ts file at the root of a Next.js project. It runs on every matching request.
Problem
Both inline entry points break on Vercel's edge runtime, at import time, in a way no application code can catch.
Vercel's edge runtime documentation lists WebAssembly.instantiate among the features that "are disabled, and will not work", with this note:
While WebAssembly.instantiate is supported in Edge Runtime, it requires the Wasm source code to be provided using the import statement. This means you cannot use a buffer or byte array to dynamically compile the module at runtime.
That is exactly what both entries do. The generator at packages/protect-ffi/scripts/inline-wasm.mjs:53-56 emits this, at the top level of the module:
const wasmBytes = Uint8Array.from(atob(WASM_BYTES_B64), (c) => c.charCodeAt(0));
const { instance } = await WebAssembly.instantiate(wasmBytes, {
"./protect_ffi_bg.js": bgImports,
});
There are two such blobs in a bundle, not one. The second comes from @cipherstash/auth, at wasm/stack_auth_wasm_inline.js:9, and does the same thing.
Why it cannot be caught
The await is at module top level, not inside a function. The failure happens while the module graph is being evaluated, before any of our code or the customer's code runs. There is no try block that can wrap it and no place to put a fallback. The customer sees the middleware fail, not a CipherStash error message.
Who hits it
Anyone who imports either inline entry from middleware.ts in a Next.js project deployed to Vercel. They do not have to choose the edge runtime — middleware selects it by default.
This is a reasonable thing for a customer to do. Middleware is where you check a session, and checking a session is a natural place to want a decrypted value. Our own documentation points people at the inline entries for edge runtimes without excluding middleware.
Why nothing catches it today
Neither package declares an edge-light or worker export condition, so there is no branch we could point at a different build. Resolution falls through to import, which is the inlined file. Nothing in our test suite runs against Vercel's edge runtime. A local next start is not a substitute, because the self-hosted edge runtime and Vercel's are not the same environment.
What we do not know yet
Two things are unverified and should not be asserted until someone checks them:
- Whether the code-size limit applies to middleware. Vercel documents a code-size limit for edge functions — 1 MB on Hobby, 2 MB on Pro, 4 MB on Enterprise, measured after gzip compression. No Vercel document states a limit for Routing Middleware specifically. If the same limit does apply, we fail that too: the
@cipherstash/stack-supabase/wasm-inline bundle measures 2,173,890 bytes gzipped, which is over both the Hobby and Pro limits.
- Whether the failure is loud or quiet. It is not established whether Vercel fails the build or fails at request time. A build failure is bad. A request-time failure on every request is worse, and would deploy green.
Proposal
- Confirm the failure with one deployment. A minimal Next.js project, a
middleware.ts that imports @cipherstash/stack/wasm-inline, deployed to Vercel. Record whether it fails at build or at request time.
- Document it as an unsupported runtime, in
skills/stash-edge/SKILL.md and the deployment guidance, in the same words the runtime list already uses. Right now the skill lists supported runtimes and says nothing about middleware, so a reader assumes it is covered by "edge".
- Fail with a useful message if we can. A top-level
await cannot be wrapped, but a runtime probe before instantiation could throw something that names the problem and the alternative, rather than letting the platform report a generic module error. Investigate whether this is possible without adding startup cost on runtimes that work.
- Decide the supported answer for Vercel and write it down. The Node.js runtime is the likely answer — the native
@cipherstash/stack entry already works there. Middleware cannot use it, so the guidance has to say what to do instead: do the encrypted work in a route handler and keep middleware to routing.
A cheaper interim step, if 1 to 4 are not scheduled soon: do step 2 alone. A documented limitation costs one paragraph and prevents the silent version of this.
Verification
Checked against bd973824.
$ sed -n '53,56p' packages/protect-ffi/scripts/inline-wasm.mjs
$ grep -n "WebAssembly.instantiate" .../@cipherstash/auth/wasm/stack_auth_wasm_inline.js
9:const { instance } = await WebAssembly.instantiate(wasmBytes, {
Neither @cipherstash/stack nor @cipherstash/stack-supabase declares any runtime export condition. The ./wasm-inline subpath of each resolves through import only.
Vercel documentation quoted above was read on 2026-08-26 at https://vercel.com/docs/functions/runtimes/edge (page metadata: last updated 2026-08-03).
Relationship to other work
Background
CipherStash Stack encrypts individual database columns. The encryption engine is written in Rust and compiled two ways: a native binary for Node.js, and a WebAssembly (WASM) build for runtimes that cannot load a native binary.
The WASM build ships through two entry points —
@cipherstash/stack/wasm-inlineand@cipherstash/stack-supabase/wasm-inline. "Inline" means the compiled WASM is embedded in the JavaScript file as a base64 string, so no separate.wasmfile has to be fetched at runtime. This is what makes the package work on Deno, Bun, Cloudflare Workers and Supabase Edge Functions.Vercel runs some code on an edge runtime, which is a restricted JavaScript environment rather than full Node.js. Vercel has deprecated standalone Edge Functions, but Routing Middleware still uses the edge runtime by default, and that is supported. Middleware is the
middleware.tsfile at the root of a Next.js project. It runs on every matching request.Problem
Both inline entry points break on Vercel's edge runtime, at import time, in a way no application code can catch.
Vercel's edge runtime documentation lists
WebAssembly.instantiateamong the features that "are disabled, and will not work", with this note:That is exactly what both entries do. The generator at
packages/protect-ffi/scripts/inline-wasm.mjs:53-56emits this, at the top level of the module:There are two such blobs in a bundle, not one. The second comes from
@cipherstash/auth, atwasm/stack_auth_wasm_inline.js:9, and does the same thing.Why it cannot be caught
The
awaitis at module top level, not inside a function. The failure happens while the module graph is being evaluated, before any of our code or the customer's code runs. There is notryblock that can wrap it and no place to put a fallback. The customer sees the middleware fail, not a CipherStash error message.Who hits it
Anyone who imports either inline entry from
middleware.tsin a Next.js project deployed to Vercel. They do not have to choose the edge runtime — middleware selects it by default.This is a reasonable thing for a customer to do. Middleware is where you check a session, and checking a session is a natural place to want a decrypted value. Our own documentation points people at the inline entries for edge runtimes without excluding middleware.
Why nothing catches it today
Neither package declares an
edge-lightorworkerexport condition, so there is no branch we could point at a different build. Resolution falls through toimport, which is the inlined file. Nothing in our test suite runs against Vercel's edge runtime. A localnext startis not a substitute, because the self-hosted edge runtime and Vercel's are not the same environment.What we do not know yet
Two things are unverified and should not be asserted until someone checks them:
@cipherstash/stack-supabase/wasm-inlinebundle measures 2,173,890 bytes gzipped, which is over both the Hobby and Pro limits.Proposal
middleware.tsthat imports@cipherstash/stack/wasm-inline, deployed to Vercel. Record whether it fails at build or at request time.skills/stash-edge/SKILL.mdand the deployment guidance, in the same words the runtime list already uses. Right now the skill lists supported runtimes and says nothing about middleware, so a reader assumes it is covered by "edge".awaitcannot be wrapped, but a runtime probe before instantiation could throw something that names the problem and the alternative, rather than letting the platform report a generic module error. Investigate whether this is possible without adding startup cost on runtimes that work.@cipherstash/stackentry already works there. Middleware cannot use it, so the guidance has to say what to do instead: do the encrypted work in a route handler and keep middleware to routing.A cheaper interim step, if 1 to 4 are not scheduled soon: do step 2 alone. A documented limitation costs one paragraph and prevents the silent version of this.
Verification
Checked against
bd973824.Neither
@cipherstash/stacknor@cipherstash/stack-supabasedeclares any runtime export condition. The./wasm-inlinesubpath of each resolves throughimportonly.Vercel documentation quoted above was read on 2026-08-26 at https://vercel.com/docs/functions/runtimes/edge (page metadata: last updated 2026-08-03).
Relationship to other work
edge-lightexport condition once a Vercel Edge run justifies it #806 asks whether to add anedge-lightexport condition for Vercel Edge. That issue is about adding a target. This one is about a target customers can already reach by default, without opting in. The two share a root cause — the buffer-based instantiation — but Add theedge-lightexport condition once a Vercel Edge run justifies it #806 can be declined without addressing this.