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
@cipherstash/stack is our TypeScript encryption SDK. Today it encrypts through @cipherstash/protect-ffi (languages/typescript/packages/protect-ffi), native bindings to our older Rust client library, cipherstash-client. Our newer Rust library, stack-encrypt (packages/stack-encrypt), is a separate engine with a different design: one chained plan builder that every caller and every language binding goes through (docs/plans/2026-10-04-plan-builder.md and ADR-0007, both on PR cipherstash/stack#1052). A plan is a saved declaration of how a value is encrypted: a context, and per field the search indexes to derive.
Problem
- Two engines. Every encryption rule exists once in cipherstash-client (for TypeScript) and once in stack-encrypt (for Rust and Go). They can disagree about bytes, and features land in one before the other.
- The TypeScript schema builder is already a plan in another spelling.
encryptedTable('users', { email: types.TextEq() }) means exactly Plan::context("users").fields().encrypt_into::<TextEq>("email"), the EQL domain as the field's target. Today that declaration drives a second engine.
- The two entries already diverge (the open issues listed below), partly because the Node entry and the
wasm-inline entry sit on different layers.
Proposal
An umbrella for a new major version of @cipherstash/stack built on stack-encrypt, retiring protect-ffi. Breaking changes are expected.
- The schema builder becomes the TypeScript spelling of a plan.
types.TextEq() lowers to encrypt_into::<TextEq>; the TS package grows no executor of its own (ADR-0007).
- The Node entry is a napi-rs shell over stack-encrypt, in-process, with its own HTTP client and stack-auth's strategies.
- The
wasm-inline entry uses the WebAssembly guest, which needs the sans-I/O ABI (#1064) because an edge runtime's event loop cannot block on a synchronous import.
- Prerequisite: stack-encrypt must read and write data in the legacy cipherstash-client format, so existing customer data keeps decrypting and existing stored terms keep matching after the upgrade.
- Re-triage these under this issue (do not close them): #797, #798, #792, #746, #949, #663, #804, #781, #561. For each, record whether the new major fixes it by construction, still needs its own change, or no longer applies.
Child issues should be filed per entry and per step once the Rust prerequisites land.
Relationship to other work
- Blocked by #1057 (plan builder), #1059 (
dynamic::record as a lowering), #1060 (JSON index) and #1062 (EQL domains as field targets), and by legacy cipherstash-client format compatibility in stack-encrypt.
- The edge (
wasm-inline) entry is additionally blocked by #1064 (sans-I/O guest ABI).
- Design:
docs/plans/2026-10-04-plan-builder.md ("Other languages") and ADR-0007 on PR cipherstash/stack#1052.
Background
@cipherstash/stackis our TypeScript encryption SDK. Today it encrypts through@cipherstash/protect-ffi(languages/typescript/packages/protect-ffi), native bindings to our older Rust client library, cipherstash-client. Our newer Rust library, stack-encrypt (packages/stack-encrypt), is a separate engine with a different design: one chained plan builder that every caller and every language binding goes through (docs/plans/2026-10-04-plan-builder.mdand ADR-0007, both on PR cipherstash/stack#1052). A plan is a saved declaration of how a value is encrypted: a context, and per field the search indexes to derive.Problem
encryptedTable('users', { email: types.TextEq() })means exactlyPlan::context("users").fields().encrypt_into::<TextEq>("email"), the EQL domain as the field's target. Today that declaration drives a second engine.wasm-inlineentry sit on different layers.Proposal
An umbrella for a new major version of
@cipherstash/stackbuilt on stack-encrypt, retiring protect-ffi. Breaking changes are expected.types.TextEq()lowers toencrypt_into::<TextEq>; the TS package grows no executor of its own (ADR-0007).wasm-inlineentry uses the WebAssembly guest, which needs the sans-I/O ABI (#1064) because an edge runtime's event loop cannot block on a synchronous import.Child issues should be filed per entry and per step once the Rust prerequisites land.
Relationship to other work
dynamic::recordas a lowering), #1060 (JSON index) and #1062 (EQL domains as field targets), and by legacy cipherstash-client format compatibility in stack-encrypt.wasm-inline) entry is additionally blocked by #1064 (sans-I/O guest ABI).docs/plans/2026-10-04-plan-builder.md("Other languages") and ADR-0007 on PR cipherstash/stack#1052.