Backend
This page walks through the API routes and server-side configuration in the quickstart repo. All paths are relative to the repo root.
Session Configuration
lib/session.ts — configures iron-session for encrypted cookie-based sessions.
iron-session wraps Next.js's cookies() API with encryption so users can't forge their session cookie. The session stores three fields:
export interface SessionData {
nonce?: string // pending SIWE nonce
address?: string // verified Ethereum address
chainId?: number // chain used during sign-in
}
Cookies are httpOnly and secure in production (HTTPS only).
SIWE Configuration
lib/siwe.ts — configures the SIWE library with an Ethereum RPC connection.
import { configure, createConfig } from '@signinwithethereum/siwe'
configure(
await createConfig(process.env.ETH_RPC_URL || 'https://eth.llamarpc.com'),
)
The RPC connection is needed for smart contract wallet verification (EIP-1271). For regular EOA wallets, signature verification is purely cryptographic — no RPC call needed. The library handles this distinction automatically.
This module is imported for its side effect (import '@/lib/siwe') by routes that call verify().
API Routes
The app has four routes. Together they implement a nonce-based authentication flow.
GET /api/nonce
app/api/nonce/route.ts — generates a random nonce using the SIWE library and stores it in the session. The frontend includes this nonce in the SIWE message, and the server checks it during verification to prevent replay attacks.
POST /api/verify
app/api/verify/route.ts — the core authentication endpoint. Receives a signed SIWE message, verifies it, and creates a session.
const siweMessage = new SiweMessage(message)
const { data } = await siweMessage.verify({
signature,
domain: process.env.NEXT_PUBLIC_DOMAIN ?? new URL(request.url).host,
nonce: session.nonce,
})
// Store verified identity in session
session.address = data.address
session.chainId = data.chainId
session.nonce = undefined // invalidate nonce — one-time use
await session.save()
The domain parameter uses NEXT_PUBLIC_DOMAIN when set, falling back to the request's Host header. Set it explicitly when running behind a reverse proxy.
GET /api/me
app/api/me/route.ts — returns the current user's address and chain ID from the session, or 401 if not authenticated.
POST /api/logout
app/api/logout/route.ts — destroys the session.
What verify() Checks
When you call siweMessage.verify(), the library validates:
- Domain binding — the message's domain matches your server (prevents phishing)
- Nonce match — the nonce matches what your server issued (prevents replay attacks)
- Signature recovery — the recovered address matches the message's claimed address
- Time validation — checks
expirationTimeandnotBeforeif present - EIP-1271 — for smart contract wallets, calls
isValidSignatureonchain
If any check fails, verify() throws a SiweError with a typed error:
import { SiweError, SiweErrorType } from '@signinwithethereum/siwe'
try {
await siweMessage.verify({ signature, domain, nonce })
} catch (error) {
if (error instanceof SiweError) {
// error.type is one of:
// EXPIRED_MESSAGE, NONCE_MISMATCH, INVALID_SIGNATURE, ...
}
}
Production Tips
For production deployments, review the full Security Considerations guide. Key recommendations:
- Use a strong session secret — at least 32 characters, loaded from environment variables
- Use a dedicated RPC provider — public RPCs have rate limits; use Alchemy, Infura, or similar
- Set short nonce expiry — nonces should expire after a few minutes
- Rate-limit the
/api/nonceand/api/verifyendpoints - Set
NEXT_PUBLIC_DOMAIN— explicitly set the expected domain for verification when behind a reverse proxy
Next Steps
- TypeScript Library Reference — full API documentation including configuration options
- Security Considerations — comprehensive security guide
- OIDC Provider — use SIWE as an OpenID Connect identity provider