Recipe · Integration · Next 14+

Next.js App Router integration

The whole thing fits in 30 lines: provider tree at the root, lazy-factory encryptor at each call site.

1. Provider tree#

WagmiProvider → QueryClientProvider → ZamaProvider. Mount these in a "use client" file at the root of your app's layout. Order matters: wagmi's hooks need QueryClient; Zama's relayer needs wagmi's wallet state.

app/providers.tsx
tsx
// app/providers.tsx
"use client";

import { WagmiProvider } from "wagmi";
import { sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ZamaProvider, RelayerWeb, indexedDBStorage } from "@zama-fhe/react-sdk";
import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
import { wagmiConfig } from "@/lib/wagmi";

const queryClient = new QueryClient();

// Construct the signer + relayer at module level — safe here because this
// file is "use client". Never build them in a server component.
const signer = new WagmiSigner({ config: wagmiConfig });
const relayer = new RelayerWeb({
  getChainId: () => signer.getChainId(),
  transports: {
    [sepolia.id]: {
      relayerUrl: "https://your-app.com/api/relayer/11155111",
      network: "https://sepolia.infura.io/v3/YOUR_KEY",
    },
  },
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <ZamaProvider relayer={relayer} signer={signer} storage={indexedDBStorage}>
          {children}
        </ZamaProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

In app/layout.tsx, wrap {children} in <Providers>.

2. Use SDK hooks in client components#

Hooks live at @tokenops/sdk/<product>/react. Every hook is a TanStack Query primitive, mutations are useMutation, reads are useQuery. Use them like any other wagmi or viem hook.

components/CreateVestingButton.tsx
tsx
// components/CreateVestingButton.tsx
"use client";

import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useCreateVesting } from "@tokenops/sdk/fhe-vesting/react";
import type { Address } from "viem";

export function CreateVestingButton({
  managerAddress,
  recipient,
}: {
  managerAddress: Address;
  recipient: Address;
}) {
  const zamaSDK = useZamaSDK();

  const create = useCreateVesting({
    address: managerAddress,
    // Lazy factory: resolves the encryptor at submit time, not at mount.
    encryptor: () => zamaSDK.relayer,
  });

  return (
    <button
      disabled={create.isPending}
      onClick={() =>
        create.mutate({
          params: {
            recipient,
            startTimestamp: Math.floor(Date.now() / 1000),
            endTimestamp: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
            cliffSeconds: 0,
            releaseIntervalSecs: 86_400,
            timelockSeconds: 0,
            initialUnlockBps: 0,
            cliffAmountBps: 0,
            isRevocable: false,
          },
          amount: 100_000n,
        })
      }
    >
      {create.isPending ? "Submitting…" : "Create vesting"}
    </button>
  );
}
Interactive · createVesting on Sepolia
Loading editor…
Edit any input above, then press to run.
Console0 lines
No output yet. Run the snippet to see logs stream in.

What server components can do#

Plenty, just not the encrypted writes. The headless client classes work fine on the server for reads: balance lookups, deployment metadata, vesting schedule shapes, address registry access. Anything that doesn't submit a tx is fair game in a server component or server action.

Optional: 2–3× faster encryption#

Want 2–3× faster encryption? Set threads: 4–8 on Zama's RelayerWeb — this requires cross-origin isolation, because multi-threaded WASM runs on SharedArrayBuffer, which browsers only enable on cross-origin-isolated pages.

app/providers.tsx
tsx
const relayer = new RelayerWeb({
  getChainId: () => signer.getChainId(),
  // Opt-in: 4-8 threads is the sweet spot; needs the COOP/COEP
  // headers below. Omit for the default single-threaded worker.
  threads: 8,
  transports: {
    /* unchanged */
  },
});
next.config.mjs
js
// next.config.mjs — cross-origin isolation for threads > 1
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
          { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
        ],
      },
    ];
  },
};

export default nextConfig;

Two things to know before flipping the switch. Cross-origin isolation is page-wide: Cross-Origin-Embedder-Policy: require-corp blocks cross-origin iframes, scripts, and images that do not send CORP or CORS headers, so audit your embeds first. And the setting fails soft: with threads > 1but no isolation, Zama's SDK logs a warning and encryption keeps working single-threaded — nothing breaks, it is just not faster. React Native is a separate story (no browser worker at all): see the React Native recipe.

See also