Used when building React frontends (Vite or Next.js App Router) that integrate with FHEVM confidential smart contracts. Covers the @zama-fhe/* SDK stack, ZamaProvider wiring, encrypt and decrypt hooks, ERC-7984 wrap and unwrap flows, and Next.js SSR or bundler configuration.
Used when building React frontends (Vite or Next.js App Router) that integrate with FHEVM confidential smart contracts. Covers the @zama-fhe/* SDK stack, ZamaProvider wiring, encrypt and decrypt hooks, ERC-7984 wrap and unwrap flows, and Next.js SSR or bundler configuration.
allowed-tools
["Read","TodoRead","TodoWrite"]
fhevm-frontend
Ship React/Next.js UIs that integrate with FHEVM confidential contracts. Covers the @zama-fhe/* SDK stack, ZamaProvider wiring, encrypt/decrypt hook flows, ERC-7984 shielding, and bundler/SSR configuration.
Compatibility note: this skill targets @zama-fhe/sdk@^3
Two SDK families coexist in Zama's npm namespace:
Low-level, @zama-fhe/relayer-sdk — what docs.zama.org currently documents (createInstance(SepoliaConfig), framework-agnostic). Targeted by older tutorials and the published fhevm-react-template.
High-level, @zama-fhe/sdk + @zama-fhe/react-sdk — what npm install @zama-fhe/react-sdk resolves. Surface: ZamaProvider, hooks, RelayerWeb / RelayerNode. This skill targets the high-level family. Version pins and verification date live in references/package-stack.md.
Concrete differences relative to docs.zama.org:
Relayer URL — https://relayer.testnet.zama.org/v2 (note /v2), shipped in SepoliaConfig.
Gateway chain ID — 10901.
ACL / KMS contract addresses differ. Use the values from SepoliaConfig exported by @zama-fhe/sdk@^3; do not copy from the doc site.
If a developer arrives from docs.zama.org and sees disagreement, this skill is correct against the npm registry — these values work against the v2 protocol. If you genuinely need the lower-level path, install @zama-fhe/relayer-sdk directly and call createInstance — out of scope for this skill.
Essential Principles
1. User decryption is reencryption, not on-chain decryption
The relayer re-encrypts a ciphertext under the user's ephemeral ML-KEM public key. Nothing is revealed on-chain; nothing is synchronous. The on-chain call returns a bytes32 handle; useUserDecrypt orchestrates the ML-KEM keypair, the EIP-712 signature, and the relayer round-trip to produce the plaintext client-side.
2. "use client" is mandatory in Next.js App Router (not in Vite)
Every component that imports from @zama-fhe/react-sdk under Next.js App Router must declare "use client" at the top. FHE operations run in a Web Worker that relies on browser APIs (window, fetch, IndexedDB). Server-side rendering crashes on import. Transitive consumers of ZamaProvider must carry the directive forward even though ZamaProvider itself ships with "use client".
Vite (and any non-SSR React stack) has no Server Component boundary, so the directive does not apply — omit it in Vite files.
3. relayerUrl is not the RPC URL
The RelayerWeb config takes a relayerUrl (the HTTPS Zama relayer service for encrypt and decrypt requests) plus a network property (the Ethereum JSON-RPC for on-chain reads). The error message "wrong relayer url" fires regardless of which one is misconfigured — including the case where undici (Node's fetch polyfill) returns an ArrayBuffer instead of Uint8Array from response.bytes(). When this error appears, verify relayerUrl, network, AND the runtime's response-type behavior.
4. Two independent authorization layers precede user decryption
On-chain ACL: FHE.allow(ciphertext, userAddress) must have been called in the contract before the relayer will re-encrypt a handle for the user. Off-chain session credential: the wallet must have signed an EIP-712 permit covering the contract address (prime with useAllow([contractAddress]), check with useIsAllowed({ contractAddresses })). Both are required — useIsAllowed reports only the session side. Miss either and the relayer rejects with DecryptionFailedError and no clear root-cause indication.
ZamaProvider calls useQueryClient() internally — QueryClientProvider must be an ancestor. WagmiSigner subscribes to wallet events via WagmiProvider. Swapping the order throws at mount.
6. Decryption requires explicit user action
useUserDecrypt is a React Query hook with mutation-grade side effects (EIP-712 signature, ML-KEM keypair, wallet prompt). Gating enabled on data readiness alone (enabled: hasHandle && hasSession) fires it on mount and surfaces unprompted wallet popups. Always gate enabled on a click-flipped userRequested boolean and reset it on handle change.usePublicDecrypt is already a mutation, no gate needed. Full diagnosis: AP-9 and references/ui-patterns.md.
Stack Support
The FHEVM semantics (principles 1, 3, 4, 5; every hook in references/hook-catalog.md) are framework-agnostic. The SSR/provider-shell patterns are Next.js App Router only; Vite gets a simpler parallel path.
Concern
Next.js App Router (14+)
Vite (5+) / other React
"use client" directive
required on every consumer (Principle 2)
not used — no Server Components
Provider shell (next/dynamic + ssr: false)
required (see workflows/configure-build.md)
not needed — instantiate at module top
Env var syntax
process.env.NEXT_PUBLIC_<NAME> (literal only)
import.meta.env.VITE_<NAME> (literal only)
Env var prefix
NEXT_PUBLIC_
VITE_
Bundler config
next.config.mjs (Turbopack on Next 16, Webpack on 14/15)
vite.config.ts + vite-plugin-node-polyfills
Dev server restart on .env edit
required
required
Dynamic env access bug
AP-7 (Next side)
AP-7 (Vite side)
Hydration mismatch risk
real — the shell avoids it
not applicable
For stack-specific code, follow workflows/setup-provider.md Phase 5 (Next shell) or references/provider-wiring.md Variant 4 (Vite direct). Other stacks (CRA, Remix, TanStack Start) are either Next-App-Router-like (apply the shell pattern) or Vite-like (instantiate directly).
When to Use
Setting up ZamaProvider with Wagmi + a signer + storage in a fresh React / Next.js / Vite app.
Calling useEncrypt to prepare an encrypted input bundle for a contract write.
Calling useUserDecrypt for a value where the current user has ACL access.
Calling usePublicDecrypt for a publicly-decryptable handle.
Using useShield / useUnshield to wrap or unwrap between ERC-20 and ERC-7984.
Configuring Webpack fallbacks, Turbopack resolveAlias, or Next.js serverExternalPackages for this stack.
Generic React, Next.js, or Wagmi help with no FHEVM content → external references.
Routing
**Step 0 — Framework (mandatory).** If the prompt does not name **Next.js (App Router)** or **Vite** (or another React stack the user has explicitly committed to), STOP and ask: *"Next.js (App Router) or Vite?"* Do not default; the SSR shell, env-var syntax, bundler config, and provider wiring all branch on this. Resume routing only after the user answers.
Step 1 — Task. Which task does the user's prompt match?
Set up ZamaProvider + signer + storage in a new app.
Encrypt a value to pass to a contract.
Decrypt a user-owned value (EIP-712 reencryption).
Decrypt a publicly-decryptable value.
Wrap an ERC-20 into ERC-7984 or unwrap back.
Configure the bundler (Next.js Turbopack / Webpack, or Vite).
Debug a runtime error (wrong relayer url, WASM 404, undici, silent decrypt, env var not inlined, zero-handle decrypt).
Deploy to production (CSP for the relayer, COOP/COEP, WASM caching, Node runtime).
If the prompt does not match any of these, explain the scope of this skill and suggest the closest alternative from When NOT to Use.
After reading a workflow file, follow it exactly. Do not invent steps.
Quick Reference
Stack
Package
Pin
Role
Install
@zama-fhe/react-sdk
^3.0.0
React hooks + ZamaProvider
direct
@zama-fhe/sdk
^3.0.0
Peer of react-sdk; hosts signer subpaths (/viem, /ethers)
direct
@zama-fhe/relayer-sdk
resolved by @zama-fhe/sdk
Low-level WASM + relayer client
transitive only
Install exactly two packages in dependencies. @zama-fhe/relayer-sdk is a regular dep of @zama-fhe/sdk and resolves automatically — do not list it directly. Node >=22. See references/package-stack.md for peer dependencies and pinning.
Deprecated → current
Obsolete
Current
Source
fhevmjs
@zama-fhe/relayer-sdk (low-level) or @zama-fhe/sdk (high-level)
2024 rename
FheProvider
ZamaProvider
react-sdk 2.x
useDecrypt
useUserDecrypt
react-sdk 2.x
FHE.requestDecryption
useUserDecrypt / usePublicDecrypt
react-sdk 2.x
SepoliaZamaFHEVMConfig
SepoliaConfig
react-sdk 2.x
Full deprecated catalog in references/anti-patterns.md.
Hooks
Full hook surface (~60 hooks) lives in references/hook-catalog.md with the most-used contracts table at the top. Canonical signatures: node_modules/@zama-fhe/react-sdk/dist/index.d.ts.
Public hooks grouped by purpose (encrypt, decrypt, token, wrap/unwrap, discovery, registry, delegation, activity); links to the canonical .d.ts for exact signatures
references/provider-wiring.md
Complete nesting code including signer variants (Wagmi, Viem, Ethers) — subpath imports, class instantiation, next/dynamic + ssr: false shell
references/anti-patterns.md
Gotchas with wrong → why-wrong → correct → verification
references/ui-patterns.md
FHEVM-specific UX patterns: explicit-action decryption, handle display, decryption lifecycle states, ACL-denied recovery, encrypt UX, wallet rejection vs real error
Workflow
Input → Output
workflows/setup-provider.md
Prompt about new-app setup → wired ZamaProvider with justified prop choices
workflows/encrypt-input.md
Prompt about encrypting → useEncrypt call + transaction submission pattern
workflows/user-decrypt.md
Prompt about reading user values → useAllow + useUserDecrypt chain
workflows/public-decrypt.md
Prompt about public decrypt → usePublicDecrypt call + proof usage
workflows/wrap-unwrap.md
Prompt about ERC-20 ↔ ERC-7984 → useShield / useUnshield orchestration
Prompt with error message → taxonomy lookup + fix steps
workflows/deploy-to-production.md
Prompt about production deploy → Node runtime, CSP for the relayer, WASM caching, COOP/COEP for multi-threaded WASM
Success Criteria
Every external link resolves.
The frontend ABI is imported from the contracts workspace's artifact (<contracts-workspace>/artifacts/contracts/<Contract>.sol/<Contract>.json) or kept identical to it. Hand-maintained ABIs that drift on the indexed flag silently corrupt event decoding — see AP-10.
dependencies holds exactly two @zama-fhe/* packages: @zama-fhe/react-sdk@^3 and @zama-fhe/sdk@^3. @zama-fhe/relayer-sdk (transitive) appears only via npm explain @zama-fhe/relayer-sdk.
Wagmi resolved to >=3.0.0 (not the declared >=2). npm ls wagmi confirms.
Every component example that imports from react-sdk declares "use client".
Signer classes are imported from their subpaths (@zama-fhe/react-sdk/wagmi, @zama-fhe/sdk/viem, @zama-fhe/sdk/ethers) and passed as instances (new WagmiSigner({ config })), not class references.
RelayerWeb config is { transports, getChainId }, with each transport spreading a preset (SepoliaConfig / MainnetConfig / HardhatConfig). See AP-4.
Every env var is read with literal dot access — process.env.NEXT_PUBLIC_X (Next) or import.meta.env.VITE_X (Vite). See AP-7.
Every useUserDecrypt call guards against the zero handle via isZeroHandle(handle). See AP-8 and workflows/user-decrypt.md Phase 1.5.
Every useUserDecrypt call gates enabled on a userRequested boolean flipped by an explicit click, with a useEffect that resets the boolean on handle change. Never enabled: hasHandle && hasSession alone. See Principle 6, AP-9, and references/ui-patterns.md "Explicit-Action-Only Decryption".
Next.js only: provider tree mounted via next/dynamic(..., { ssr: false }) shell. See workflows/setup-provider.md Phase 5a and workflows/configure-build.md § SSR shell. Vite: no shell; module-top mount.
useEncrypt's type field uses plain string literals (type: "euint64") — no as FheTypeName cast. The discriminated union rejects the widened type.
tsconfig.jsoncompilerOptions.target is ES2020+ (BigInt literals). See workflows/setup-provider.md Phase 1.5.
Next.js >=16: either (a) package.json scripts pass --webpack on every next dev / next build, or (b) next.config.* has a turbopack key. Turbopack is the default bundler in Next 16.
Next.js: next.config file is .mjs / .cjs / .ts as appropriate for package.json's "type" — a .js with require() under "type": "module" throws at load.
Vite: vite.config file is .ts (recommended) or .mjs. Under "type": "module", a .js with require() or a .cjs that uses ESM imports breaks the same way as Next — keep the extension aligned with the package.json type.
Vite: vite.config.ts includes vite-plugin-node-polyfills for Buffer (and optional globals). See workflows/configure-build.md Phase 3.
No example uses fhevmjs, FheProvider, useDecrypt, FHE.requestDecryption, or SepoliaZamaFHEVMConfig.
Build exits 0 under CI.npm run build (Next) or npm run build runs to completion with no errors.
Runtime mount is verified manually (the agent does NOT skip this step — it surfaces the checklist to the user). Build success is necessary but not sufficient: env-var static-replacement bugs (AP-7), zero-handle errors (AP-8), and bundler / SSR shell misconfiguration (workflows/configure-build.md) only surface at mount or interaction time. Prompt the user to run npm run dev, open http://localhost:<port> in a browser, open DevTools, and confirm:
Console — no ReferenceError, no Cannot find module, no [zama-sdk] DecryptCache.clearAll failed.
Network tab — every .wasm request returns HTTP 200.
Mounted state — a component calling useZamaSDK() renders without throwing.
Generated code drops into the target stack without edits beyond contract addresses and secrets. Contract address env vars follow the convention NEXT_PUBLIC_<NAME>_ADDRESS (Next) or VITE_<NAME>_ADDRESS (Vite) — e.g. NEXT_PUBLIC_COUNTER_ADDRESS / VITE_COUNTER_ADDRESS.