iron-session is a stateless, encrypted cookie session library — getIronSession<SessionData>(req, res, options) reads or creates a session from an encrypted cookie. session.save() encrypts and sets the cookie. session.destroy() clears it. Session data is typed via a SessionData interface augmentation. password must be a 32-character string stored in an environment variable. cookieName, cookieOptions.secure, cookieOptions.httpOnly, and cookieOptions.sameSite configure cookie behavior. Multiple passwords in an array enable key rotation without invalidating old sessions. iron-session works in Next.js App Router (Server Actions, Route Handlers), Pages Router (withIronSessionApiRoute/withIronSessionSsr), and Cloudflare Workers Edge Runtime. It stores all session data in the cookie itself — no database required. Claude Code generates iron-session auth flows, shopping cart sessions, CSRF nonce patterns, and multi-step form state.
CLAUDE.md for iron-session
## iron-session Stack
- Version: iron-session >= 8.0
- Session: const session = await getIronSession<SessionData>(req, res, sessionOptions)
- Read: session.userId — returns undefined if not set
- Write: session.userId = user.id; await session.save()
- Destroy: await session.destroy()
- Options: { cookieName: "app_session", password: process.env.SESSION_SECRET!, cookieOptions: { secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "lax" } }
- Type: declare module "iron-session" { interface IronSessionData { userId?: string } }
- Rotation: password: [{ id: 2, password: newSecret }, { id: 1, password: oldSecret }]
Session Configuration
// lib/session/config.ts — shared session options and types
import type { IronSessionOptions } from "iron-session"
// Extend IronSessionData to add your session fields
declare module "iron-session" {
interface IronSessionData {
userId?: string
role?: "user" | "admin" | "moderator"
cartId?: string
csrfToken?: string
onboarding?: {
step: number
email?: string
plan?: string
}
flash?: {
type: "success" | "error" | "info"
message: string
}
}
}
// Single config for App Router and Pages Router
export const sessionOptions: IronSessionOptions = {
cookieName: "app_session",
// Array supports key rotation — newest first
password: process.env.SESSION_SECRET_V2
? [
{ id: 2, password: process.env.SESSION_SECRET_V2 },
{ id: 1, password: process.env.SESSION_SECRET! },
]
: process.env.SESSION_SECRET!,
cookieOptions: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 30 - 60, // 30 days minus 1 minute
path: "/",
},
}
App Router — Server Actions
// lib/session/actions.ts — Server Actions for session management
"use server"
import { getIronSession } from "iron-session"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { revalidatePath } from "next/cache"
import { sessionOptions } from "./config"
import { db } from "@/lib/db"
import { verifyPassword } from "@/lib/auth/passwords"
import { z } from "zod"
async function getSession() {
return getIronSession(await cookies(), sessionOptions)
}
// Login action — call from a form
export async function loginAction(formData: FormData) {
const parsed = z.object({
email: z.string().email(),
password: z.string().min(1),
}).safeParse(Object.fromEntries(formData))
if (!parsed.success) {
return { error: "Invalid credentials" }
}
const { email, password } = parsed.data
const user = await db.user.findUnique({ where: { email } })
if (!user || !(await verifyPassword(password, user.passwordHash))) {
// Constant-time comparison to prevent timing attacks
return { error: "Invalid email or password" }
}
const session = await getSession()
session.userId = user.id
session.role = user.role as "user" | "admin"
await session.save()
redirect("/dashboard")
}
// Logout action
export async function logoutAction() {
const session = await getSession()
await session.destroy()
redirect("/sign-in")
}
// Get current user — use in Server Components
export async function getCurrentUser() {
const session = await getSession()
if (!session.userId) return null
return db.user.findUnique({
where: { id: session.userId },
select: { id: true, name: true, email: true, role: true, avatarUrl: true },
})
}
// Require auth — throw redirect if not logged in
export async function requireAuth() {
const session = await getSession()
if (!session.userId) redirect("/sign-in")
return session.userId
}
// Flash messages — set on action, consume on next render
export async function setFlash(type: "success" | "error" | "info", message: string) {
const session = await getSession()
session.flash = { type, message }
await session.save()
}
export async function getAndClearFlash() {
const session = await getSession()
const flash = session.flash
if (flash) {
delete session.flash
await session.save()
}
return flash ?? null
}
Route Handler — Session API
// app/api/session/route.ts — REST session endpoints
import { NextRequest, NextResponse } from "next/server"
import { getIronSession } from "iron-session"
import { sessionOptions } from "@/lib/session/config"
import { db } from "@/lib/db"
import { z } from "zod"
// GET /api/session — return current session state (for client components)
export async function GET(request: NextRequest) {
const response = NextResponse.next()
const session = await getIronSession(request, response, sessionOptions)
if (!session.userId) {
return NextResponse.json({ authenticated: false })
}
const user = await db.user.findUnique({
where: { id: session.userId },
select: { id: true, name: true, email: true, role: true },
})
if (!user) {
// User was deleted — clear stale session
await session.destroy()
return NextResponse.json({ authenticated: false })
}
return NextResponse.json({ authenticated: true, user })
}
// DELETE /api/session — logout
export async function DELETE(request: NextRequest) {
const response = NextResponse.json({ ok: true })
const session = await getIronSession(request, response, sessionOptions)
await session.destroy()
return response
}
Cart Session
// lib/session/cart.ts — guest cart in session, merge on login
import { getIronSession } from "iron-session"
import { cookies } from "next/headers"
import { sessionOptions } from "./config"
import { db } from "@/lib/db"
export type CartItem = {
productId: string
quantity: number
variantId?: string
}
export async function getSessionCart(): Promise<CartItem[]> {
// Session stores cart as JSON in cookie for guests
const session = await getIronSession(await cookies(), sessionOptions)
if (session.userId) {
// Logged-in user — use database cart
const cart = await db.cart.findFirst({
where: { userId: session.userId },
include: { items: true },
})
return cart?.items.map(i => ({ productId: i.productId, quantity: i.quantity })) ?? []
}
// Guest — cart lives in session cookie
const raw = (session as any).guestCart as CartItem[] | undefined
return raw ?? []
}
export async function addToSessionCart(item: CartItem): Promise<void> {
const session = await getIronSession(await cookies(), sessionOptions)
if (session.userId) {
// Logged-in user — persist to DB
await db.cartItem.upsert({
where: { cartId_productId: { cartId: session.cartId!, productId: item.productId } },
create: { cartId: session.cartId!, ...item },
update: { quantity: { increment: item.quantity } },
})
return
}
// Guest — update session cookie
const guestCart = ((session as any).guestCart as CartItem[] | undefined) ?? []
const existing = guestCart.find(i => i.productId === item.productId)
if (existing) {
existing.quantity += item.quantity
} else {
guestCart.push(item)
}
;(session as any).guestCart = guestCart
await session.save()
}
// Merge guest cart into user cart after login
export async function mergeGuestCart(userId: string): Promise<void> {
const session = await getIronSession(await cookies(), sessionOptions)
const guestCart = ((session as any).guestCart as CartItem[] | undefined) ?? []
if (guestCart.length === 0) return
await db.$transaction(
guestCart.map(item =>
db.cartItem.upsert({
where: { cartId_productId: { cartId: userId, productId: item.productId } },
create: { cartId: userId, ...item },
update: { quantity: { increment: item.quantity } },
}),
),
)
delete (session as any).guestCart
await session.save()
}
CSRF Protection
// lib/session/csrf.ts — CSRF token generation and validation
import { getIronSession } from "iron-session"
import { cookies } from "next/headers"
import { sessionOptions } from "./config"
export async function generateCsrfToken(): Promise<string> {
const session = await getIronSession(await cookies(), sessionOptions)
if (!session.csrfToken) {
const bytes = crypto.getRandomValues(new Uint8Array(32))
session.csrfToken = Buffer.from(bytes).toString("base64url")
await session.save()
}
return session.csrfToken
}
export async function validateCsrfToken(token: string): Promise<boolean> {
const session = await getIronSession(await cookies(), sessionOptions)
if (!session.csrfToken) return false
// Constant-time comparison
const a = Buffer.from(token)
const b = Buffer.from(session.csrfToken)
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
}
// Server Action usage:
// const token = await generateCsrfToken() // in Server Component
// <input type="hidden" name="csrf_token" value={token} />
// In Server Action: if (!await validateCsrfToken(formData.get("csrf_token") as string)) throw new Error("CSRF")
For the jose JWT alternative when stateless token verification across multiple services, API authentication with Authorization: Bearer headers, or Edge Runtime JWT signing without a cookie is preferred — jose tokens are portable across services while iron-session cookies only work in the browser-to-server flow, see the jose guide. For the Lucia Auth alternative when a full session management library with database-backed sessions, multi-device session listing, and granular session revocation are needed — Lucia stores sessions in your database for auditability while iron-session cookies are entirely stateless and cannot be individually revoked, see the Lucia guide. The Claude Skills 360 bundle includes iron-session skill sets covering auth sessions, cart state, and CSRF protection. Start with the free tier to try encrypted session generation.