Skip to content

[bug] [SSR] WagmiAdapter singleton causes cross-request state leakage in Next.js App Router #5571

@yrjkqq

Description

@yrjkqq

Link to minimal reproducible example

https://github.com/yrjkqq/demo/tree/main/packages/web https://demo-web-two-green.vercel.app/aa

Steps to Reproduce

Steps to Reproduce

1. Setup (following official docs)

config.ts — module-level singleton as recommended:

import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";
import { cookieStorage, createStorage } from "wagmi";
import { sepolia } from "@reown/appkit/networks";

export const wagmiAdapter = new WagmiAdapter({
  networks: [sepolia],
  projectId: "YOUR_PROJECT_ID",
  ssr: true,
  storage: createStorage({ storage: cookieStorage }),
});

providers.tsx — client component:

"use client";
import { WagmiProvider } from "wagmi";
import { createAppKit } from "@reown/appkit/react";
import { wagmiAdapter } from "./config";

createAppKit({
  adapters: [wagmiAdapter],
  networks: [sepolia],
  projectId: "YOUR_PROJECT_ID",
});

export function Providers({ children, initialState }) {
  return (
    <WagmiProvider config={wagmiAdapter.wagmiConfig} initialState={initialState}>
      {children}
    </WagmiProvider>
  );
}

layout.tsx — server component with cookie hydration:

import { headers } from "next/headers";
import { cookieToInitialState } from "wagmi";
import { wagmiAdapter } from "./config";
import { Providers } from "./providers";

export default async function Layout({ children }) {
  const initialState = cookieToInitialState(
    wagmiAdapter.wagmiConfig,
    (await headers()).get("cookie")
  );
  return <Providers initialState={initialState}>{children}</Providers>;
}

page.tsx — displays connected wallet:

"use client";
import { useAccount } from "wagmi";

export default function Page() {
  const { address, isConnected } = useAccount();
  return <p>{isConnected ? `Connected: ${address}` : "Not connected"}</p>;
}

2. Reproduce the leak

  1. Open Browser A, visit the page, connect a wallet (e.g. 0xF9ea...8FFC)
  2. Open Browser B (different browser or incognito), visit the same page — do NOT connect any wallet
  3. In Browser B, open DevTools → Network → select the HTML request → Response tab

3. Observe

Browser B's server-rendered HTML response contains Browser A's wallet address:

<p>Connected: 0xF9ea...8FFC</p>

Despite Browser B sending no cookies in the request.

Image Image

Summary

Description

When following the official AppKit + Next.js documentation, the WagmiAdapter is created as a module-level singleton. Combined with cookieToInitialState() from wagmi, this causes cross-request state leakage during SSR — one user's wallet connection state bleeds into another user's server-rendered HTML.

Root Cause

wagmiAdapter is a Node.js module-level singleton. Its internal wagmiConfig maintains a store that persists across all requests in the same Node.js process.

When cookieToInitialState() is called for Request A (with cookies), it hydrates the singleton config's internal store with the connected wallet state. This state is never reset between requests, so Request B (without cookies) reads the stale state from Request A during SSR.

Request A (has cookie)
  → cookieToInitialState() writes to wagmiAdapter.wagmiConfig store
  → SSR renders "Connected: 0xF9ea..." ✅

Request B (no cookie, different user)
  → cookieToInitialState() returns null
  → BUT wagmiAdapter.wagmiConfig store still has Request A's state
  → SSR renders "Connected: 0xF9ea..." ❌ ← state leakage!

Security Impact

  • Information disclosure: User B sees User A's wallet address in server-rendered HTML
  • UI flash: Server renders "connected" state, client hydration corrects to "not connected" (visual glitch)
  • Potentially worse: If the page renders balances, transaction history, or other wallet-specific data during SSR, more sensitive information could leak

Suggested Fix

Option 1: Per-request config (recommended)

The documentation should recommend creating a new WagmiAdapter instance per request, similar to how wagmi's SSR docs use a getConfig() function:

export function getWagmiAdapter() {
  return new WagmiAdapter({
    networks: [sepolia],
    projectId: "YOUR_PROJECT_ID",
    ssr: true,
    storage: createStorage({ storage: cookieStorage }),
  });
}

Option 2: Reset store between requests

WagmiAdapter could internally reset its config store when initialState is provided via cookieToInitialState, preventing stale state from persisting.

Option 3: Document the limitation

At minimum, add a warning in the Next.js SSR documentation about the singleton pattern and cross-request state leakage.

List of related npm package versions

Environment

  • @reown/appkit: 1.8.18
  • @reown/appkit-adapter-wagmi: 1.8.18
  • wagmi: 2.19.5
  • next: 16.1.6
  • react: 19.2.3

Node.js Version

v25.6.0

Package Manager

pnpm@10.2.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions