siwe
Sign-In with Ethereum (SIWE) for Scaffold-ETH 2
Prerequisites
Check if ./packages/nextjs/scaffold.config.ts exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
Critical: Use viem's Native SIWE — NOT the siwe npm Package
Viem provides all SIWE utilities natively via viem/siwe. Do not install the siwe npm package. It pulls in ethers as a peer dependency, which is unnecessary since SE-2 already uses viem.
Here are some commonly useful imports (but check official docs for any updates or alternatives):
import {
createSiweMessage,
parseSiweMessage,
verifySiweMessage,
generateSiweNonce,
} from "viem/siwe";
Dependencies
yarn workspace @se-2/nextjs add iron-session
Gotchas
1. Domain Validation in the Verify Route
The verify route must validate the SIWE message's domain against the request's Host header. Without this, an attacker can replay a signature from a different domain. This is a critical security check that's easy to skip:
// In your verify API route
const expectedDomain = req.headers.get("host");
if (!expectedDomain) {
return NextResponse.json({ error: "Missing Host header" }, { status: 400 });
}
// Pass domain to verifySiweMessage
const isValid = await verifySiweMessage(client, {
message,
signature,
nonce: storedNonce,
domain: expectedDomain, // CRITICAL — validates domain match
});
2. Session Options Must Use a Lazy Getter
sessionOptions must NOT be a module-level constant that calls getSessionPassword() at import time. During next build, the code runs in production mode, and the env var won't be set, causing a build failure. Use a lazy factory:
// packages/nextjs/utils/siwe.ts
import { SessionOptions } from "iron-session";
export type SiweSessionData = {
nonce?: string;
address?: string;
chainId?: number;
isLoggedIn: boolean;
signedInAt?: string;
};
export const defaultSession: SiweSessionData = { isLoggedIn: false };
// Lazy getter — defers env var evaluation to request time
export function getSessionOptions(): SessionOptions {
const secret = process.env.IRON_SESSION_SECRET;
const password =
secret && secret.length >= 32
? secret
: process.env.NODE_ENV === "production"
? (() => {
throw new Error(
"IRON_SESSION_SECRET must be set in production (32+ chars)",
);
})()
: "complex_password_at_least_32_characters_long_for_dev";
return {
password,
cookieName: "siwe-session",
cookieOptions: {
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60,
},
};
}
3. hasSeenWalletConnected Ref to Prevent False Auto-Logout
On page refresh, the wallet reconnects asynchronously, causing a brief isConnected: false state. Without tracking whether the wallet was ever connected, this triggers a false logout. Use a ref:
const hasSeenWalletConnected = useRef(false);
useEffect(() => {
if (isConnected) {
hasSeenWalletConnected.current = true;
}
if (!isConnected && hasSeenWalletConnected.current && state.isSignedIn) {
// Wallet actually disconnected — sign out
fetch("/api/siwe/session", { method: "DELETE" }).then(() => {
setState((prev) => ({ ...prev, isSignedIn: false, address: undefined }));
});
}
}, [isConnected, state.isSignedIn]);
ERC-6492 Smart Wallet Support
The verify route should create a publicClient per chain to support smart contract wallet (Safe, Argent) signature verification. Maintain a SUPPORTED_CHAINS map and reject unknown chains:
import { createPublicClient, http, type Chain } from "viem";
import { mainnet, sepolia, hardhat /* ... */ } from "viem/chains";
const SUPPORTED_CHAINS: Record<number, Chain> = {
[mainnet.id]: mainnet,
[sepolia.id]: sepolia,
[hardhat.id]: hardhat,
};
// In verify route:
const chain = SUPPORTED_CHAINS[parsedMessage.chainId!];
if (!chain)
return NextResponse.json({ error: "Unsupported chain" }, { status: 400 });
const client = createPublicClient({ chain, transport: http() });
More from scaffold-eth/scaffold-eth-2
solidity-security
Master smart contract security best practices to prevent common vulnerabilities and implement secure Solidity patterns. Use when writing smart contracts, auditing existing contracts, or implementing security measures for blockchain applications.
10defi-protocol-templates
Implement DeFi protocols with production-ready templates for staking, AMMs, governance, and lending systems. Use when building decentralized finance applications or smart contract protocols.
10ponder
Integrate Ponder into a Scaffold-ETH 2 project for blockchain event indexing. Use when the user wants to: index contract events, add a blockchain backend, set up GraphQL for onchain data, use Ponder with SE-2, or build an indexer for their dApp.
7erc-20
Add an ERC-20 token contract to a Scaffold-ETH 2 project. Use when the user wants to: create a fungible token, deploy an ERC-20, add token minting, build a token transfer UI, or work with ERC-20 tokens in SE-2.
6erc-721
Add an ERC-721 NFT contract to a Scaffold-ETH 2 project. Use when the user wants to: create an NFT collection, deploy an ERC-721, add NFT minting, build an NFT gallery or transfer UI, or work with non-fungible tokens in SE-2.
5eip-5792
Add EIP-5792 batched transaction support to a Scaffold-ETH 2 project. Use when the user wants to: batch multiple contract calls, use wallet_sendCalls, add EIP-5792 wallet integration, batch onchain transactions, or use wagmi's experimental batch hooks.
4