sdk/js-sdk/docs/architecture.md
This page explains how @fhevm/sdk is built internally. It is for contributors
and for anyone who wants to understand why the API looks the way it does.
Application developers can skip it — nothing here is required to use the SDK.
The SDK is one package with clear internal boundaries rather than many packages. Everything is organized in layers, each of which knows only about the layer below it:
┌───────────────────────────────────────────────────────────────┐
│ @fhevm/sdk/ethers @fhevm/sdk/viem │ Adapters (~200 LOC each)
│ createFhevmClient, setFhevmRuntimeConfig, init helpers │
├───────────────────────────────────────────────────────────────┤
│ Client + decorators │ Methods live here
│ base / encrypt / decrypt action groups attached via extend() │
├───────────────────────────────────────────────────────────────┤
│ Actions (@fhevm/sdk/actions/*) │ Pure functions (fhevm, params)
├───────────────────────────────────────────────────────────────┤
│ Runtime modules (encrypt · decrypt · relayer · ethereum) │ Orchestration + I/O
├───────────────────────────────────────────────────────────────┤
│ WASM (TFHE · TKMS) │ Cryptography
└───────────────────────────────────────────────────────────────┘
A client is a small object carrying a shared internal context (chain,
runtime, options) with action methods decorated onto it. Each method is a thin
wrapper over a standalone action function that takes the client as its first
argument — which is exactly why @fhevm/sdk/actions/* exists as a public,
tree-shakable surface.
| Path | Responsibility |
|---|---|
src/ethers/, src/viem/ | Thin adapters: factories, runtime config, native signing. |
src/core/clients/ | Client construction and the decorator groups. |
src/core/actions/ | Standalone action functions (base/encrypt/decrypt/chain/host). |
src/core/runtime/ | The composable runtime and its lazy init. |
src/core/modules/ | Runtime modules: encrypt, decrypt, relayer, ethereum. |
src/core/chains/ | Built-in chain definitions and defineFhevmChain. |
src/core/kms/ | Transport key pair, permits, KMS share flow. |
src/core/coprocessor/ | ZK proof building. |
src/core/handle/, src/core/types/ | Encrypted-value handles and the type system. |
src/core/host-contracts/ | Reads against ACL / verifier host contracts. |
src/wasm/ | The TFHE and TKMS WASM libraries and loaders. |
The ethers and viem adapters are deliberately tiny. Everything real lives in
core; an adapter only knows how to read a contract, sign typed data, and derive
a chain id through its native library.
A FhevmRuntime is a composable container of modules. A module wraps a unit
of capability — and, for the two cryptographic modules, a specific WASM library:
Modules are added by extension, which is the mechanism behind the four client
factories. createFhevmEncryptClient extends the runtime with only the encrypt
module, so a bundler never pulls in the decrypt WASM. createFhevmClient extends
with both.
The SDK follows a strict initialization contract:
init() resolves versions and loads WASM. It runs every module's
registered init function: it resolves the protocol, TFHE, and TKMS versions and
compiles the WASM the client's modules need. ready is a getter alias for
init(). Awaiting either is required before encryptValues, decryptValue, or
generateTransportKeyPair — those actions assert the versions are already
resolved and throw otherwise.init() is idempotent. Calling it again returns the same cached promise.
decryptPublicValues is the exception that needs no prior init(): it resolves
what it needs on demand (the protocol version comes from the on-chain ACL
contract version, read over RPC) rather than requiring the versions to be
pre-resolved.This is what lets construction stay cheap while giving callers explicit control
over when the latency of WASM compilation is paid — you await ready once, at a
moment you choose.
encryptValues is a two-stage pipeline split across the encrypt and relayer
modules:
inputProof a contract trusts.These stages are also exposed as the standalone actions generateZkProof and
fetchEncryptedValues, so advanced callers can separate proof creation from
verification.
decryptValue composes the KMS share flow:
Because reconstruction is local, the plaintext never crosses the network in the clear.
The internals follow a consistent set of rules:
type: string) and are validated when a call runs, so the SDK
throws clear errors rather than over-constraining the type system.name, with structured context — never
by parsing message strings.