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
- Open Browser A, visit the page, connect a wallet (e.g.
0xF9ea...8FFC)
- Open Browser B (different browser or incognito), visit the same page — do NOT connect any wallet
- 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.
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
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:
providers.tsx— client component:layout.tsx — server component with cookie hydration:
page.tsx — displays connected wallet:
2. Reproduce the leak
0xF9ea...8FFC)3. Observe
Browser B's server-rendered HTML response contains Browser A's wallet address:
Despite Browser B sending no cookies in the request.
Summary
Description
When following the official AppKit + Next.js documentation, the
WagmiAdapteris created as a module-level singleton. Combined withcookieToInitialState()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
wagmiAdapteris a Node.js module-level singleton. Its internalwagmiConfigmaintains 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.Security Impact
Suggested Fix
Option 1: Per-request config (recommended)
The documentation should recommend creating a new
WagmiAdapterinstance per request, similar to how wagmi's SSR docs use agetConfig()function:Option 2: Reset store between requests
WagmiAdaptercould internally reset its config store wheninitialStateis provided viacookieToInitialState, 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.18wagmi: 2.19.5next: 16.1.6react: 19.2.3Node.js Version
v25.6.0
Package Manager
pnpm@10.2.1