Updated 2026-10-05 (PDT). The Go SDK design is PR cipherstash/stack#1070: docs/plans/2026-10-04-plan-builder.md, "The Go SDK", and the principles in docs/sdk-design-principles.md (ADR-0008). Consistent with this issue. ADR-0008 records the principles every language SDK follows; the TypeScript schema builder is the TypeScript spelling of a declaration. The napi shell links the same generated EQL dispatch as the Go guest, so the EQL decision for Go is the decision for every binding.
Background
stack-encrypt, our Rust field-level encryption library, runs inside a WebAssembly (WASI) guest for hosts that cannot or should not load a native library: Go today (languages/golang/stackencrypt/guest/, hosted by wazero), and in future the edge runtimes (Cloudflare Workers, Supabase Edge, Deno Deploy, browsers). The guest reaches ZeroKMS, our key management service, through one host import, transport_send (packages/stack-guest-abi/src/transport.rs).
Problem
transport_send is synchronous from the guest's point of view, and the ABI relies on that ("block_on never parks"). Under wazero a host function may block a goroutine, so Go is fine. In a browser, Deno, Bun or an edge worker, a WebAssembly import may not block the event loop, so the guest as built cannot run there. Asyncify, JSPI and a worker with Atomics.wait are each fragile workarounds.
Proposal
From "Where the design strains", item 2, of docs/plans/2026-10-04-plan-builder.md (PR cipherstash/stack#1052):
- Make the guest sans-I/O: export the two halves of a batch. The first export builds a plan's
Pending and returns the ZeroKMS requests it needs; the host performs the round trip in its own idiom (a fetch promise, a goroutine); the second export takes the responses and finishes. Pending already separates building requests from dispatching them, so this exposes an existing seam rather than adding one.
- Keep the synchronous path for Go if it stays simpler there, or move Go onto the same two halves; decide in the PR.
- Required before the TypeScript
wasm-inline edge binding is built, not before. Native shells (napi-rs, PyO3) never meet this.
Relationship to other work
- Blocked by #1059 (
dynamic::record as a lowering, so the guest runs the builder).
- Blocks the edge (
wasm-inline) entry of #1065.
- Design:
docs/plans/2026-10-04-plan-builder.md on PR cipherstash/stack#1052.
Background
stack-encrypt, our Rust field-level encryption library, runs inside a WebAssembly (WASI) guest for hosts that cannot or should not load a native library: Go today (
languages/golang/stackencrypt/guest/, hosted by wazero), and in future the edge runtimes (Cloudflare Workers, Supabase Edge, Deno Deploy, browsers). The guest reaches ZeroKMS, our key management service, through one host import,transport_send(packages/stack-guest-abi/src/transport.rs).Problem
transport_sendis synchronous from the guest's point of view, and the ABI relies on that ("block_onnever parks"). Under wazero a host function may block a goroutine, so Go is fine. In a browser, Deno, Bun or an edge worker, a WebAssembly import may not block the event loop, so the guest as built cannot run there. Asyncify, JSPI and a worker withAtomics.waitare each fragile workarounds.Proposal
From "Where the design strains", item 2, of
docs/plans/2026-10-04-plan-builder.md(PR cipherstash/stack#1052):Pendingand returns the ZeroKMS requests it needs; the host performs the round trip in its own idiom (afetchpromise, a goroutine); the second export takes the responses and finishes.Pendingalready separates building requests from dispatching them, so this exposes an existing seam rather than adding one.wasm-inlineedge binding is built, not before. Native shells (napi-rs, PyO3) never meet this.Relationship to other work
dynamic::recordas a lowering, so the guest runs the builder).wasm-inline) entry of #1065.docs/plans/2026-10-04-plan-builder.mdon PR cipherstash/stack#1052.