> For the complete documentation index, see [llms.txt](https://docs.zama.org/protocol/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zama.org/protocol/sdk/guides/configuration.md).

# Configuration

How to configure the SDK with createConfig — chains, relayers, provider, signer, and storage.

The SDK uses `createConfig` to wire together chains, relayers, a provider, an optional signer, and storage into a single configuration object. This guide walks through each piece.

## Steps

### 1. Pick your chains

Import pre-configured chain objects from `@zama-fhe/sdk/chains`. Each chain includes contract addresses, relayer URLs, and chain IDs.

```ts
import { sepolia, mainnet, hoodi } from "@zama-fhe/sdk/chains";
```

| Chain          | Chain ID   | Description             |
| -------------- | ---------- | ----------------------- |
| `mainnet`      | `1`        | Ethereum Mainnet        |
| `polygon`      | `137`      | Polygon Mainnet         |
| `sepolia`      | `11155111` | Sepolia Testnet         |
| `polygonAmoy`  | `80002`    | Polygon Amoy Testnet    |
| `hoodi`        | `560048`   | Hoodi Testnet           |
| `ingenTestnet` | `364301`   | InGen Testnet           |
| `bscTestnet`   | `97`       | BNB Smart Chain Testnet |
| `hardhat`      | `31337`    | Local Hardhat node      |

`anvil` is also exported as an alias for `hardhat` (both target chain ID `31337`), for Foundry users.

{% hint style="info" %}
The shared Zama testnet relayer needs **no API key**: presets like `sepolia` and `polygonAmoy` work as-is, so leave `auth` unset. Only the Zama-hosted **mainnet** relayer (used by `mainnet` and `polygon`) requires a key; see [Authentication](/protocol/sdk/guides/authentication.md).
{% endhint %}

### 2. Pick a relayer

Relayers tell the SDK how to run FHE operations on each chain.

| Relayer       | Environment | Description                                  |
| ------------- | ----------- | -------------------------------------------- |
| `web()`       | Browser     | Runs FHE via bundled WASM in the browser     |
| `node()`      | Node.js     | Same FHE runtime, server-side                |
| `cleartext()` | Local dev   | No FHE infrastructure — cleartext operations |

```ts
import { cleartext } from "@zama-fhe/sdk";
import { web } from "@zama-fhe/sdk/web";
import { node } from "@zama-fhe/sdk/node";
```

Chain-specific data (`relayerUrl`, `network`, `executorAddress`, etc.) comes from the chain preset, so a bare call is all most apps need. Each factory also accepts an optional options object forwarded to `@fhevm/sdk` for per-client tuning (e.g. `batchRpcCalls`, `fheEncryptionKey`, `moduleVersions`, `timeout`) — see the [`web()`](/protocol/sdk/api-references/sdk/relayerweb.md#parameters) / [`node()`](/protocol/sdk/api-references/sdk/relayernode.md#parameters) / [`cleartext()`](/protocol/sdk/api-references/sdk/relayercleartext.md#parameters) reference for the full option list and defaults.

`web()` additionally runs encryption in a dedicated Web Worker by default, so `encryptValue()`/`encryptValues()` calls don't block the main thread. Tune or opt out of this with `offloadEncrypt`, `offloadWorker`, and `offloadTimeouts` — see [`web()`'s encryption offload options](/protocol/sdk/api-references/sdk/relayerweb.md#offloadencrypt) for the full behavior and defaults. If your app sets a Content Security Policy, it needs `worker-src 'self' blob:` for the offload worker to start.

```ts
// Browser — uses relayerUrl from the chain preset
web();

// Node.js — chain data comes from the preset
node();

// Local dev — no KMS, no gateway; executorAddress comes from the chain preset
cleartext();
```

The `relayers` map is keyed by chain id, one entry per chain in `chains`. A chain with no entry fails `createConfig` with a `ConfigurationError`. An entry whose chain is not in `chains` is unused and only warns through the configured logger, so a static relayer catalog can serve an environment-filtered chain list.

If you need to override a chain field (e.g. proxy relayer requests through your backend), spread the preset in the `chains` array:

```ts
import { sepolia, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  relayerUrl: "https://your-app.com/api/relayer/11155111",
} as const satisfies FheChain;
```

### 3. Set up chain access

The SDK separates read access (provider) from wallet authority (signer). The provider handles contract reads and receipt polling. The signer handles signing and write transactions. Both are created automatically by `createConfig` — you pass your Web3 library's native objects.

{% tabs %}
{% tab title="wagmi (React)" %}

```tsx
// createConfig from @zama-fhe/react-sdk/wagmi accepts your wagmiConfig directly — see step 4 below.
```

{% endtab %}

{% tab title="viem" %}

```ts
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { sepolia } from "viem/chains";

const publicClient = createPublicClient({
  chain: sepolia,
  transport: http("https://sepolia.infura.io/v3/YOUR_KEY"),
});
const walletClient = createWalletClient({ chain: sepolia, transport: custom(window.ethereum!) });
```

{% endtab %}

{% tab title="ethers" %}

```ts
// Browser — pass the raw EIP-1193 provider
// createConfig({ ..., ethereum: window.ethereum! })

// Node.js — pass an ethers Signer (provider is extracted automatically)
// const provider = new ethers.JsonRpcProvider(rpcUrl);
// createConfig({ ..., signer: new ethers.Wallet(privateKey, provider) })
```

{% endtab %}
{% endtabs %}

For full type information, see the [ViemProvider](/protocol/sdk/api-references/sdk/viemprovider.md) / [ViemSigner](/protocol/sdk/api-references/sdk/viemsigner.md) and [EthersProvider](/protocol/sdk/api-references/sdk/ethersprovider.md) / [EthersSigner](/protocol/sdk/api-references/sdk/etherssigner.md) reference pages. You can also implement [GenericProvider](/protocol/sdk/api-references/sdk/genericprovider.md) and [GenericSigner](/protocol/sdk/api-references/sdk/genericsigner.md) for a custom integration.

### 4. Create the config

`createConfig` takes your chains, relayers, and signer adapter and returns a config object.

{% tabs %}
{% tab title="React + wagmi" %}

```tsx
import { web } from "@zama-fhe/sdk/web";
import { createConfig as createZamaConfig } from "@zama-fhe/react-sdk/wagmi";
import { sepolia, mainnet, type FheChain } from "@zama-fhe/sdk/chains";

// Override relayerUrl to proxy through your backend
const mySepolia = {
  ...sepolia,
  relayerUrl: "https://your-app.com/api/relayer/11155111",
} as const satisfies FheChain;
const myMainnet = {
  ...mainnet,
  relayerUrl: "https://your-app.com/api/relayer/1",
} as const satisfies FheChain;

const zamaConfig = createZamaConfig({
  chains: [mySepolia, myMainnet],
  wagmiConfig,
  relayers: { [mySepolia.id]: web(), [myMainnet.id]: web() },
});
```

{% endtab %}

{% tab title="Browser (viem)" %}

```ts
import { createConfig } from "@zama-fhe/sdk/viem";
import { ZamaSDK } from "@zama-fhe/sdk";
import { web } from "@zama-fhe/sdk/web";
import { sepolia, mainnet, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  relayerUrl: "https://your-app.com/api/relayer/11155111",
} as const satisfies FheChain;
const myMainnet = {
  ...mainnet,
  relayerUrl: "https://your-app.com/api/relayer/1",
} as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia, myMainnet],
  publicClient,
  walletClient,
  relayers: { [mySepolia.id]: web(), [myMainnet.id]: web() },
});

const sdk = new ZamaSDK(config);
```

{% endtab %}

{% tab title="Browser (ethers)" %}

```ts
import { createConfig } from "@zama-fhe/sdk/ethers";
import { ZamaSDK } from "@zama-fhe/sdk";
import { web } from "@zama-fhe/sdk/web";
import { sepolia, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  relayerUrl: "https://your-app.com/api/relayer/11155111",
} as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia],
  ethereum: window.ethereum!,
  relayers: { [mySepolia.id]: web() },
});

const sdk = new ZamaSDK(config);
```

{% endtab %}

{% tab title="Node.js" %}

```ts
import { createConfig } from "@zama-fhe/sdk/viem";
import { ZamaSDK, memoryStorage } from "@zama-fhe/sdk";
import { node } from "@zama-fhe/sdk/node";
import { sepolia, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  network: "https://sepolia.infura.io/v3/YOUR_KEY",
} as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia],
  publicClient,
  walletClient,
  storage: memoryStorage,
  relayers: { [mySepolia.id]: node() },
});

const sdk = new ZamaSDK(config);
```

{% endtab %}

{% tab title="Custom signer/provider" %}
When the built-in adapters don't fit your setup — for example, a server-side relayer that implements `GenericSigner` directly — use the generic `createConfig` from `@zama-fhe/sdk`:

```ts
import { createConfig, ZamaSDK, memoryStorage } from "@zama-fhe/sdk";
import { node } from "@zama-fhe/sdk/node";
import { sepolia, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  network: "https://sepolia.infura.io/v3/YOUR_KEY",
} as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia],
  signer: myCustomSigner, // implements GenericSigner
  provider: myCustomProvider, // implements GenericProvider
  storage: memoryStorage,
  relayers: { [mySepolia.id]: node() },
});

const sdk = new ZamaSDK(config);
```

See [GenericSigner](/protocol/sdk/api-references/sdk/genericsigner.md) and [GenericProvider](/protocol/sdk/api-references/sdk/genericprovider.md) for the interfaces your adapter must implement.
{% endtab %}

{% tab title="Web Extensions" %}
MV3 Chrome extensions can use `chromeSessionStorage` as `permitStorage` so permits survive service worker restarts:

```ts
import { createConfig } from "@zama-fhe/sdk/viem";
import { ZamaSDK, indexedDBStorage, chromeSessionStorage } from "@zama-fhe/sdk";
import { web } from "@zama-fhe/sdk/web";
import { sepolia, type FheChain } from "@zama-fhe/sdk/chains";

const mySepolia = {
  ...sepolia,
  relayerUrl: "https://your-app.com/api/relayer/11155111",
} as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia],
  publicClient,
  walletClient,
  storage: indexedDBStorage,
  permitStorage: chromeSessionStorage,
  relayers: { [mySepolia.id]: web() },
});

const sdk = new ZamaSDK(config);
```

Your `manifest.json` must include the `"storage"` permission. See the [Web Extensions guide](/protocol/sdk/guides/web-extensions.md) for manifest configuration, multi-context sharing, and browser close behavior.
{% endtab %}
{% endtabs %}

Browser apps should proxy relayer requests through a backend to keep the API key secret. See the [Authentication guide](/protocol/sdk/guides/authentication.md) for the full setup.

### 5. (Optional) Configure TTLs and event listener

You can tune how long the transport key pair and permits remain valid, and subscribe to lifecycle events for debugging:

```ts
const config = createConfig({
  chains: [sepolia],
  wagmiConfig,
  relayers: { [sepolia.id]: web() },
  transportKeyPairTTL: 604800, // 7 days in seconds (default: 2592000 = 30 days)
  permitTTL: 7, // 7 days (default: 30 days)
  onEvent: ({ type, tokenAddress, ...rest }) => {
    console.debug(`[zama] ${type}`, rest);
  },
});
```

When done with the SDK, call `sdk.terminate()` to unsubscribe wallet listeners and release the SDK's resources.

### 6. (Optional) Choose a storage backend

The transport key pair is cached so users don't get a wallet popup on every decrypt. By default, `createConfig` picks the right storage for your environment. Override with the `storage` field if needed:

| Storage             | When to use                                               |
| ------------------- | --------------------------------------------------------- |
| `indexedDBStorage`  | Browser apps — persists across reloads and sessions       |
| `memoryStorage`     | Tests, scripts, throwaway sessions                        |
| `asyncLocalStorage` | Node.js servers — isolates transport key pair per request |

```ts
import { indexedDBStorage, memoryStorage } from "@zama-fhe/sdk";
// Node.js per-request isolation:
// import { asyncLocalStorage } from "@zama-fhe/sdk/node";
```

For full storage options see the [GenericStorage](/protocol/sdk/api-references/sdk/genericstorage.md) reference.

### 7. (Optional) Supply a logger

The SDK is **silent by default** — it emits no console output of its own. Operation failures always surface through the rejected promise or typed error, never as a stray `console.error`. To observe internal diagnostics, pass a `logger` to `createConfig`:

```ts
const config = createConfig({
  chains: [sepolia],
  wagmiConfig,
  relayers: { [sepolia.id]: web() },
  logger: console, // or a pino / winston / OpenTelemetry DiagLogger instance
});
```

The `logger` is a minimal four-level interface — `error`, `warn`, `info`, `debug` — that `console` and common logging libraries satisfy directly, so no adapter is needed. The SDK never bundles a logging library or imposes a format; level filtering is left to your logger. Levels follow these conventions:

| Level   | What the SDK emits                                                                           |
| ------- | -------------------------------------------------------------------------------------------- |
| `error` | Unexpected internal failures only — never failures already surfaced via a rejection          |
| `warn`  | Recoverable or degraded conditions (a fallback path, a retry, a swallowed best-effort write) |
| `info`  | Reserved for coarse lifecycle milestones; not currently emitted                              |
| `debug` | Verbose diagnostics — relayer request timing, orchestration progress                         |

The logger is configured once here and flows SDK-wide — including into relayer request tracing, the credential store, and the decrypt cache. There is deliberately no per-relayer logger option; `createConfig({ logger })` is the single source of truth.

### 8. (Optional) Tune FHE runtime performance and behavior

The `runtime` field configures the underlying `@fhevm/sdk` WASM runtime — how WASM assets load, threading, and module versions. It is process-global: it applies once per process, not per chain or per relayer.

Leaving `runtime` unset is a valid default: the SDK tries to run multi-threaded automatically, and falls back to single-threaded on its own when the environment doesn't support it (see `numberOfThreads`/`singleThread` below).

```ts
const config = createConfig({
  chains: [sepolia],
  wagmiConfig,
  relayers: { [sepolia.id]: web() },
  runtime: {
    numberOfThreads: 4, // parallelize FHE work across Web Workers
  },
});
```

Every field is optional:

| Field               | Default                                    | Effect                                                                                                                                                                                                                                                                                                                    |
| ------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `numberOfThreads`   | requested: `navigator.hardwareConcurrency` | Number of Web Workers to parallelize FHE encryption/decryption across. This is the *requested* count — the *effective* count degrades to single-threaded (see `singleThread` below) where `navigator` is unavailable (Node < 21, some edge runtimes), or where `SharedArrayBuffer`/a spawnable worker isn't.              |
| `singleThread`      | `false`                                    | `true` forces a single thread — no `SharedArrayBuffer` required. The SDK also auto-upgrades to this at runtime whenever multi-threading was requested but can't actually run (no cross-origin isolation, no way to spawn a worker for the chosen `wasmAssetLoadMode`) — silently, with a console warning, never an error. |
| `wasmAssetLoadMode` | `"auto"`                                   | How the TFHE/KMS WASM assets are fetched and instantiated (modes below).                                                                                                                                                                                                                                                  |
| `moduleVersions`    | `"auto"`                                   | Pin the TFHE/KMS WASM module versions instead of auto-resolving them from the chain's on-chain protocol version. `checkCompatibility` — checked only when a concrete version is pinned — defaults to `"throw"`; also accepts `"warn"` or `"off"`.                                                                         |
| `locateFile`        | none                                       | Remap where the WASM assets are served from — set this when self-hosting them.                                                                                                                                                                                                                                            |
| `auth`              | none                                       | Process-wide fallback relayer authentication, applied to any chain that doesn't set its own `auth`. See [Authentication](/protocol/sdk/guides/authentication.md) for the auth methods.                                                                                                                                    |

`wasmAssetLoadMode` controls how the FHE WASM assets are fetched and instantiated:

| Mode                  | Behavior                                                                                        |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `auto` (default)      | Pick the best mode available in the current environment.                                        |
| `embedded-base64`     | Use the WASM inlined as base64 — no separate network fetch. Useful under strict CSP or offline. |
| `verified-blob`       | Fetch the WASM, verify its integrity, then instantiate from the verified blob.                  |
| `precheck-direct-url` | Load directly from the asset URL after a precheck request.                                      |
| `trusted-direct-url`  | Load directly from the asset URL with no precheck (fastest, least defensive).                   |

`runtime.logger` is not one of these — it's managed by the SDK. Pass your logger via `createConfig`'s top-level [`logger`](#7-optional-supply-a-logger) instead (step 7 above).

{% hint style="warning" %}
**`runtime` is applied once per process and can't be changed afterward.** The `@fhevm/sdk` runtime is a process-global singleton, applied by the first `createConfig` call. A later `createConfig` never reconfigures it — the original stays in effect, and a warning is logged (`"runtime configuration is already set and cannot be changed."`). Set `runtime` on your first `createConfig`; per-chain tuning that must vary belongs in each transport factory's `options` (see [Shared relayer options](#shared-relayer-options) below), not in `runtime`.
{% endhint %}

{% hint style="warning" %}
Multi-threaded FHE relies on `SharedArrayBuffer`, which browsers only expose to [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) pages. To run more than one thread, serve your app with both headers:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

If you can't set those headers (some static hosts and embedded contexts), pass `runtime: { singleThread: true }` instead — the SDK then runs FHE on the main thread with no `SharedArrayBuffer` dependency.
{% endhint %}

### 9. (Optional) Share one transport key pair across signers (B2B2C / WaaS)

By default, every signer gets its own transport key pair. Wallet-as-a-Service operators managing many end-user wallets from one operator-controlled key store can opt into sharing a single key pair across signers with `transportKeyPairScope`:

```ts
const config = createConfig({
  chains: [sepolia],
  wagmiConfig,
  relayers: { [sepolia.id]: web() },
  transportKeyPairScope: "tenant-123", // opaque identifier, e.g. your tenant ID
  storage: myPersistentStorage, // must be shared across every signer in this scope
});
```

Permits stay per-signer regardless of scope. See [Security Model](/protocol/sdk/concepts/security-model.md#shared-tenant-scope-b2b2c-waas-operators) for the tradeoff this makes, and [Permit Model](/protocol/sdk/concepts/permit-model.md#two-revocation-tiers-with-a-shared-scope) for how revocation splits into a signer-level tier (`revokePermits`/`clear`) and an operator-level one (`sdk.permits.revokeTransportKeyPair()`).

### 10. (Optional) Wrap the transport key pair at rest (headless environments)

By default the SDK stores the transport private key in plaintext and delegates at-rest security to your storage backend. `transportKeyPairDerivationSecret` encrypts the key before every write instead. It exists for headless environments that have no secure backend to delegate to. Find your environment:

| Your environment                                              | What to do                                                                                                                                                    |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser dApp                                                  | Nothing. IndexedDB behind same-origin isolation and OS disk encryption is the intended default. The constructor rejects this option.                          |
| React Native / mobile                                         | Use a platform keychain (iOS Keychain, Android Keystore) as the `storage` backend. React Native defines `window`, so the constructor rejects this option too. |
| Backend with a KMS, Vault, or encrypted storage               | Nothing. Your storage is already secure; wrapping on top adds key management without adding security.                                                         |
| Headless with plain storage (CLI tool, agent, bare-metal box) | Pass `transportKeyPairDerivationSecret`, plus a persistent `storage`.                                                                                         |

For the last row, two things must hold:

1. **The secret arrives out of band**: from the process environment, a secrets manager, or a KMS-unwrapped blob. Never store it next to the data it protects.
2. **Storage is persistent.** The headless default is in-memory, and a wrapped key that never reaches disk protects nothing.

```ts
import { createConfig } from "@zama-fhe/sdk/viem";
import { ZamaSDK } from "@zama-fhe/sdk";
import { node } from "@zama-fhe/sdk/node";
import { sepolia } from "@zama-fhe/sdk/chains";

const config = createConfig({
  chains: [sepolia],
  publicClient,
  walletClient,
  relayers: { [sepolia.id]: node() },
  storage: myPersistentStorage, // required: the headless default is in-memory
});

const sdk = new ZamaSDK(config, {
  transportKeyPairDerivationSecret: process.env.ZAMA_DERIVATION_SECRET, // string | Uint8Array
});
```

Pass `process.env.ZAMA_DERIVATION_SECRET` straight through, without asserting it with `!` first. If the value is `undefined` (an unset env var), the constructor throws `ConfigurationError` instead of silently downgrading to plaintext.

{% hint style="danger" %}
**Never ship the secret in a bundle.** Bundlers inline env values at build time, so a bundled secret reaches every copy of the artifact and protects nothing. The constructor rejects the option when it detects a browser context, but it cannot detect every bundle: keeping the secret out of shipped code is your responsibility.
{% endhint %}

The SDK never persists or exposes this value. See [Security Model](/protocol/sdk/concepts/security-model.md#wrapped-at-rest-transportkeypairderivationsecret) for the mechanism, the entropy requirement (32 random bytes, or a 64+ character string), the anti-patterns to avoid, and rotation.

## Shared relayer options

When multiple chains use the same relayer, create it once and reference that single instance from each chain:

```ts
import { sepolia, mainnet, type FheChain } from "@zama-fhe/sdk/chains";

const sharedWeb = web({ batchRpcCalls: true });

const mySepolia = { ...sepolia, relayerUrl: "/api/relayer/11155111" } as const satisfies FheChain;
const myMainnet = { ...mainnet, relayerUrl: "/api/relayer/1" } as const satisfies FheChain;

const config = createConfig({
  chains: [mySepolia, myMainnet],
  publicClient,
  walletClient,
  relayers: { [mySepolia.id]: sharedWeb, [myMainnet.id]: sharedWeb },
});
```

Chains that reference the *same* relayer object — the result of a single `web()` call — share one FHE backend instance, reducing memory usage.

## Next steps

* [Authentication](/protocol/sdk/guides/authentication.md) — set up a backend proxy or use a direct API key
* [Shield Tokens](/protocol/sdk/guides/shield-tokens.md) — convert public ERC-20 tokens into confidential form
* [Chain Objects](/protocol/sdk/api-references/sdk/network-presets.md) — pre-configured chain definitions for Sepolia, Mainnet, and more
* [GenericStorage reference](/protocol/sdk/api-references/sdk/genericstorage.md) — custom storage implementations


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.zama.org/protocol/sdk/guides/configuration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
