sdk/js-sdk/docs/clients.md
A client is the object you call to encrypt and decrypt. It binds a chain definition to a read connection and lazily loads the WebAssembly cryptography it needs.
Every client comes from one of two adapter entry points — @fhevm/sdk/ethers or
@fhevm/sdk/viem. The API is identical across both; only the native connection
object differs (provider for ethers, publicClient for viem).
Four factories exist. They differ only in which WASM modules they load and therefore which methods they expose. Pick the narrowest one your page needs — a page that only reads public values should never download the 4.9 MB encryption module.
| Factory | Encrypts | Decrypts | WASM loaded |
|---|---|---|---|
createFhevmClient | ✅ | ✅ | TFHE (~4.9 MB) + TKMS (~600 KB) |
createFhevmEncryptClient | ✅ | — | TFHE (~4.9 MB) |
createFhevmDecryptClient | — | ✅ | TKMS (~600 KB) |
createFhevmBaseClient | — | — | None |
"Decrypts" above covers private decryption (decryptValue). Reading
public values (decryptPublicValue) and signing permits are available on
every client, including the base client — they need no cryptography WASM. See
Decryption for the distinction.
{% tabs %} {% tab title="ethers.js" %}
import { createFhevmClient } from '@fhevm/sdk/ethers';
import { sepolia } from '@fhevm/sdk/chains';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://ethereum-sepolia-rpc.publicnode.com');
const client = createFhevmClient({ chain: sepolia, provider });
The provider is any ethers ContractRunner —
a JsonRpcProvider, BrowserProvider, or a connected Wallet/Signer. The
client only reads through it; it never sends transactions on your behalf.
{% endtab %} {% tab title="viem" %}
import { createFhevmClient } from '@fhevm/sdk/viem';
import { sepolia } from '@fhevm/sdk/chains';
import { createPublicClient, http } from 'viem';
import { sepolia as viemSepolia } from 'viem/chains';
const publicClient = createPublicClient({
chain: viemSepolia,
transport: http('https://ethereum-sepolia-rpc.publicnode.com'),
});
const client = createFhevmClient({ chain: sepolia, publicClient });
The publicClient is a viem PublicClient.
The client only reads through it.
{% endtab %} {% endtabs %}
Note the two sepolia imports in the viem example: @fhevm/sdk/chains supplies
the FHEVM chain definition (contract addresses, Relayer URL), while
viem/chains supplies viem's own transport chain. They are different objects and
both are needed.
Neither factory takes a signer or walletClient. A client is read-only. When
an operation needs a signature — only signDecryptionPermit
does — you pass the signer to that method:
await client.signDecryptionPermit({
/* … */
signerAddress: await signer.getAddress(),
signer, // ethers Signer, or a viem Account / WalletClient
});
This keeps encryption and public reads completely wallet-free.
Each factory accepts an optional options object:
const client = createFhevmClient({
chain: sepolia,
provider,
options: {
batchRpcCalls: true, // batch the client's on-chain reads into multicalls
},
});
| Option | Type | Applies to | Purpose |
|---|---|---|---|
batchRpcCalls | boolean | all clients | Coalesce the client's contract reads into batched RPC calls. |
fheEncryptionKey | FheEncryptionKeyBytes | encrypt / full | Provide a pre-fetched FHE public key to skip the network fetch. |
moduleVersions | FhevmModuleVersions | varies by client | Pin specific TFHE/TKMS WASM versions instead of the defaults. |
Module version pinning and the encryption-key cache are covered in Runtime configuration.
Constructing a client is instant and does no I/O. Before you encrypt or decrypt,
await client.ready (or client.init()) once — it resolves the protocol
versions and downloads and compiles the WASM the client needs. encryptValues,
decryptValue, and generateTransportKeyPair require this and throw if it hasn't
happened; decryptPublicValues is the exception — it resolves what it needs from
the Relayer and works without a prior init().
Await it at a moment you control — a splash screen, a route transition:
const client = createFhevmClient({ chain: sepolia, provider });
await client.ready; // resolves versions, downloads + compiles WASM, once
Every client exposes a small set of lifecycle members:
| Member | Type | Description |
|---|---|---|
init() | () => Promise<void> | Preload and compile the client's WASM modules. |
ready | Promise<void> | Resolves once the client is ready to use. |
uid | string | Stable identifier for this client instance. |
chain | FhevmChain | The chain definition the client was created with. |
protocolVersion | resolution object | The resolved FHEVM protocol version for the chain. |
extend(actions) | function | Attach additional action methods (advanced / internal). |
Calling init() twice is safe — module initialization is cached per WASM
version. A given WASM version is owned by a single client instance; creating a
second client that tries to load the same version throws. In practice, create
one client per page and reuse it.
{% hint style="info" %}
Constructing a client costs no download. Await client.ready once — at a moment you control, such as a splash screen or route transition — to resolve versions and compile WASM before you encrypt or decrypt.
{% endhint %}
A common pattern splits a dApp by route. A "submit" page that only encrypts:
import { createFhevmEncryptClient } from '@fhevm/sdk/ethers';
const client = createFhevmEncryptClient({ chain: sepolia, provider });
// exposes encryptValue / encryptValues (+ public-decrypt + permit helpers)
A "results" page that only reads back private values:
import { createFhevmDecryptClient } from '@fhevm/sdk/ethers';
const client = createFhevmDecryptClient({ chain: sepolia, provider });
// exposes decryptValue / decryptValues / generateTransportKeyPair (+ public-decrypt + permit helpers)
Each downloads only the WASM it needs. See Encryption and Decryption for the method details.