Claude Code for iron-session: Encrypted Cookie Sessions — Claude Skills 360 Blog
Blog / Backend / Claude Code for iron-session: Encrypted Cookie Sessions
Backend

Claude Code for iron-session: Encrypted Cookie Sessions

Published: May 3, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free