sdk/js-sdk/docs/runtime-configuration.md
Before creating any clients, you must call setFhevmRuntimeConfig() once to configure how the SDK runs its internal WASM modules. This page covers every configuration option, environment-specific behavior, and framework setup.
The simplest valid configuration:
import { setFhevmRuntimeConfig } from "@fhevm/sdk/viem"; // or "@fhevm/sdk/ethers"
setFhevmRuntimeConfig({});
All parameters are optional. This works in both Node.js and browsers with sensible defaults.
setFhevmRuntimeConfig({
numberOfThreads: 4,
singleThread: false,
locateFile: (file) => new URL(`/wasm/${file}`, window.location.origin),
logger: { debug: console.log, error: console.error },
});
| Option | Type | Default | Description |
|---|---|---|---|
numberOfThreads | number | All available cores | How many threads WASM uses for encryption |
singleThread | boolean | false | Force single-threaded mode (disables Web Workers) |
locateFile | (file: string) => URL | Auto-detect (see below) | Custom resolver for WASM file locations |
logger | { debug, error } | undefined | Optional logger for SDK debug output |
numberOfThreadsControls the number of worker threads used for TFHE encryption. More threads means faster encrypt() calls.
setFhevmRuntimeConfig({
numberOfThreads: navigator.hardwareConcurrency || 4,
});
node:worker_threads pool sizenavigator.hardwareConcurrency in browsers, all cores in Node.js)0 or less, falls back to single-threaded modeDecryption (TKMS) does not use multi-threading — it's a much lighter operation (~600KB WASM).
singleThreadForces single-threaded mode. Use this when you can't set the COOP/COEP headers required for SharedArrayBuffer in browsers (see Multi-threading in browsers).
setFhevmRuntimeConfig({
singleThread: true,
});
When true, encryption runs on the main thread. It's slower but works everywhere — no headers needed, no Web Worker restrictions.
locateFileTells the SDK where to find its WASM files. The function receives a filename and must return a URL pointing to that file.
setFhevmRuntimeConfig({
locateFile: (file) => new URL(`https://cdn.example.com/wasm/${file}`),
});
The SDK resolves these files:
| File | Size | Module | Purpose |
|---|---|---|---|
tfhe_bg.v1.5.3.wasm | ~5MB | Encryption (TFHE) | FHE encryption WASM binary |
tfhe-worker.v1.5.3.mjs | ~2KB | Encryption (TFHE) | Web Worker script for multi-threaded encryption |
kms_lib_bg.wasm | ~600KB | Decryption (TKMS) | KMS decryption WASM binary |
When you don't provide locateFile:
__filename (CJS) or import.meta.url (ESM). No configuration needed.loggerOptional logger for debug and error output. Useful for diagnosing WASM initialization issues.
setFhevmRuntimeConfig({
logger: {
debug: (message: string) => console.log("[fhevm]", message),
error: (message: string, cause: unknown) => console.error("[fhevm]", message, cause),
},
});
The logger interface:
type Logger = {
readonly debug: (message: string) => void;
readonly error: (message: string, cause: unknown) => void;
};
Pass console directly if you want all output:
setFhevmRuntimeConfig({
logger: console,
});
setFhevmRuntimeConfig() can only be called once per adapter. Calling it again with the same parameters is fine (idempotent). Calling with different parameters throws an error.
// First call — sets the config
setFhevmRuntimeConfig({ numberOfThreads: 4 });
// Same config — no-op (safe)
setFhevmRuntimeConfig({ numberOfThreads: 4 });
// Different config — throws an error
setFhevmRuntimeConfig({ numberOfThreads: 8 }); // ❌ Error
This prevents accidental reconfiguration in apps with multiple entry points or hot module reloading. If you're using a framework with HMR (like Next.js or Vite in dev mode), wrap the call in a guard:
let configured = false;
if (!configured) {
try {
setFhevmRuntimeConfig({ numberOfThreads: 4 });
configured = true;
} catch {
// Already configured from a previous HMR cycle
}
}
Node.js requires no special configuration. The SDK resolves WASM file paths automatically and uses node:worker_threads for multi-threading.
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/ethers";
import { sepolia } from "@fhevm/sdk/chains";
import { ethers } from "ethers";
setFhevmRuntimeConfig({ numberOfThreads: 4 });
const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
const client = createFhevmClient({ chain: sepolia, provider });
Requirements:
__filename in CJS and import.meta.url in ESM — handled automaticallyIn browsers, the SDK works out of the box with embedded base64 WASM. For production, you'll want to configure two things: WASM file hosting (for smaller bundles) and HTTP headers (for multi-threading).
The TFHE WASM module uses SharedArrayBuffer for multi-threading, which browsers only enable when your server sends these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Without these headers, the SDK falls back to single-threaded mode automatically. Encryption still works — it's just slower.
By default, the SDK embeds WASM as base64 in your JavaScript bundle. This is convenient (zero config) but adds ~5MB to your bundle. For production, serve the WASM files from your static assets or a CDN:
/public/wasm/)locateFile to that directory:setFhevmRuntimeConfig({
numberOfThreads: navigator.hardwareConcurrency || 4,
locateFile: (file) => new URL(`/wasm/${file}`, window.location.origin),
});
This replaces the ~5MB base64-inlined WASM with a standard HTTP download on first use.
Add COOP/COEP headers to next.config.js:
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
},
];
},
};
Configure the runtime once at app startup (e.g., in a top-level layout or provider component):
"use client";
import { setFhevmRuntimeConfig } from "@fhevm/sdk/ethers";
let configured = false;
if (!configured) {
try {
setFhevmRuntimeConfig({ numberOfThreads: 4 });
configured = true;
} catch {
// Already configured (HMR in dev mode)
}
}
Add COOP/COEP headers to vite.config.ts:
export default defineConfig({
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
});
Then configure the runtime in your app entry:
import { setFhevmRuntimeConfig } from "@fhevm/sdk/viem";
setFhevmRuntimeConfig({
numberOfThreads: navigator.hardwareConcurrency || 4,
});
Add to your server or location block:
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
Browser extensions run in a restricted environment. Key considerations:
SharedArrayBuffer — use singleThread: trueRecommended extension configuration:
setFhevmRuntimeConfig({
singleThread: true, // safest for extensions
locateFile: (file) => new URL(`wasm/${file}`, chrome.runtime.getURL("/")),
});
If your extension page controls its own headers and needs faster encryption, you can enable multi-threading:
setFhevmRuntimeConfig({
numberOfThreads: 4,
locateFile: (file) => new URL(`wasm/${file}`, chrome.runtime.getURL("/")),
});
WASM modules load lazily — not when you call setFhevmRuntimeConfig() or createFhevmClient(), but the first time you call an action that needs them:
| First call to... | What loads | Size |
|---|---|---|
encrypt() | TFHE WASM + network public key | ~5MB WASM + ~50MB key download |
decrypt() | TKMS WASM | ~600KB |
publicDecrypt() | Nothing (HTTP only) | — |
signDecryptionPermit() | Nothing | — |
generateE2eTransportKeypair() | TKMS WASM | ~600KB |
If you want to preload WASM at app startup (for example, behind a loading spinner), call:
await client.init();
// or equivalently:
await client.ready;
Both return a promise that resolves when all modules for that client type are initialized. Calling either multiple times is safe (idempotent).
"FhevmRuntime config has already been set and cannot be changed"
You called setFhevmRuntimeConfig() twice with different parameters. This usually happens with HMR in development. Wrap the call in a guard (see Idempotency).
Encryption is slow in the browser
Check that COOP/COEP headers are set correctly. Open the browser console — the SDK logs a warning if SharedArrayBuffer is not available and it falls back to single-threaded mode.
"Missing locate file function"
The browser can't find WASM files and the base64 fallback failed. Provide a locateFile function or ensure your bundler includes the SDK's embedded WASM chunks.
Worker creation fails (CSP error)
Your Content Security Policy blocks blob: URLs or inline workers. Either update your CSP to allow worker-src blob: or use locateFile to point to hosted worker scripts.