Secure, stateless, and cookie-based session library for Next.js or any JavaScript framework
# iron-session [](https://github.com/vvo/iron-session/blob/master/LICENSE) [](https://www.npmjs.com/package/iron-session)
**`iron-session` is a secure, stateless, and cookie-based session library for JavaScript.**
> [!IMPORTANT]
> **Coming from v8?** Read [Upgrading to v9](#upgrading-to-v9) for the two changes
> most apps need, or [MIGRATION.md](./MIGRATION.md) for the full guide. v9 needs
> Node 22.13+ and is ESM-only.
---
The session data is stored in signed and encrypted cookies which are decoded by your server code in a stateless fashion (= no network involved). This is the same technique used by frameworks like
[Ruby On Rails](https://guides.rubyonrails.org/security.html#session-storage).
Online demo and examples: https://get-iron-session.vercel.app
Featured in the Next.js documentation ⭐️
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Upgrading to v9](#upgrading-to-v9)
- [Usage](#usage)
- [Examples](#examples)
- [Runtimes](#runtimes)
- [Session size](#session-size)
- [Watching for unreadable cookies](#watching-for-unreadable-cookies)
- [Validating session data](#validating-session-data)
- [Project status](#project-status)
- [Session options](#session-options)
- [API](#api)
- [`getIronSession(req, res, sessionOptions): Promise>`](#getironsessiontreq-res-sessionoptions-promiseironsessiont)
- [`getIronSession(cookieStore, sessionOptions): Promise>`](#getironsessiontcookiestore-sessionoptions-promiseironsessiont)
- [`nodeCookies`, `webCookies`, `nextProxyCookies`](#nodecookiesreq-res-webcookiesrequest-responseorheaders-nextproxycookiesrequest-response)
- [`session.save(): Promise`](#sessionsave-promisevoid)
- [`session.destroy(): void`](#sessiondestroy-void)
- [`session.updateConfig(sessionOptions: SessionOptions): void`](#sessionupdateconfigsessionoptions-sessionoptions-void)
- [`sealData(data: unknown, { password, ttl }): Promise`](#sealdatadata-unknown--password-ttl--promisestring)
- [`unsealData(seal: string, { password, ttl }): Promise`](#unsealdatatseal-string--password-ttl--promiset)
- [FAQ](#faq)
- [Why use pure cookies for sessions?](#why-use-pure-cookies-for-sessions)
- [How to invalidate sessions?](#how-to-invalidate-sessions)
- [Can I use something else than cookies?](#can-i-use-something-else-than-cookies)
- [How is this different from JWT?](#how-is-this-different-from-jwt)
- [Credits](#credits)
- [Good Reads](#good-reads)
## Installation
```sh
pnpm add iron-session
```
v9 needs **Node 22.13 or later** and is **ESM-only**. `require()` still works on
Node 22.13+, which supports `require()` of an ES module. If you are stuck on an
older Node, stay on v8: `pnpm add iron-session@8`.
## Upgrading to v9
Most apps change two things. Both are things v8 got wrong quietly.
**1. Store timestamps, not `Date` objects.**
```diff
- session.lastSeen = new Date();
+ session.lastSeen = Date.now();
```
v8 turned a `Date` into a string when sealing, so the type you wrote was not the
type you read back. v9 throws and names the field.
**2. Handle a session that does not exist yet.**
```diff
- const userId = session.user.id;
+ const userId = session.user?.id;
```
Reads are typed as `Partial` now. A first visit, an expired cookie and a
`destroy()` all leave you an empty object, so the old type let this compile and
then throw at runtime.
Nothing else is required. `getIronSession(req, res, options)` and
`getIronSession(await cookies(), options)` both still work, v9 reads v8 cookies
and v8 reads v9 cookies, so you can roll a deploy back without signing everyone
out. If you had `as any` on `await cookies()`, delete it.
Worth adopting while you are here:
- [`nextProxyCookies`](#runtimes) if you ever tried to save a session in Next.js
middleware and it did not stick.
- [`onUnsealError`](#watching-for-unreadable-cookies) to see why cookies get
rejected instead of guessing.
- [`chunk: true`](#session-size) if your session outgrew one cookie.
The full guide, including the removed APIs and the security fix that signs pre-v8
cookies out once, is in [MIGRATION.md](./MIGRATION.md).
## Usage
_We have extensive examples here too: https://get-iron-session.vercel.app/._
To get a session, there's a single method to know: `getIronSession`.
```ts
// Next.js API Routes and Node.js/Express/Connect.
import { getIronSession } from "iron-session";
export async function get(req, res) {
const session = await getIronSession(req, res, { password: "...", cookieName: "..." });
return session;
}
export async function post(req, res) {
const session = await getIronSession(req, res, { password: "...", cookieName: "..." });
session.username = "Alison";
await session.save();
}
```
```ts
// Next.js Route Handlers (App Router)
import { cookies } from "next/headers";
import { getIronSession } from "iron-session";
export async function GET() {
const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
return session;
}
export async function POST() {
const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
session.username = "Alison";
await session.save();
}
```
```tsx
// Next.js Server Components and Server Actions (App Router)
import { cookies } from "next/headers";
import { getIronSession } from "iron-session";
async function getIronSessionData() {
const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
return session;
}
async function Profile() {
const session = await getIronSessionData();
return
;
}
```
```ts
// Next.js proxy.ts (middleware.ts before Next 16)
import { NextResponse, type NextRequest } from "next/server";
import { getIronSession, nextProxyCookies } from "iron-session";
export async function proxy(request: NextRequest) {
const response = NextResponse.next();
const session = await getIronSession(nextProxyCookies(request, response), options);
session.lastSeen = Date.now();
await session.save();
return response;
}
```
Middleware needs the adapter because Next only merges a cookie into the current
render when it goes through `response.cookies.set()`. Writing a raw `Set-Cookie`
header there looks like it works and then has no effect.
## Examples
Runnable examples for every pattern: https://get-iron-session.vercel.app/. Two
of them are where to start, and they follow the
[Next.js authentication guide](https://nextjs.org/docs/app/guides/authentication):
- [Server Components and Server Actions](https://get-iron-session.vercel.app/app-router-server-component-and-action)
([source](./examples/next/src/app/app-router-server-component-and-action)).
A form posts to a Server Action, the action writes the session, the page reads
it on the server. This is the default.
- [Cache Components and Partial Prerendering](https://get-iron-session.vercel.app/app-router-cache-components)
([source](./examples/next/src/app/app-router-cache-components)), for Next.js
16 with `cacheComponents` on. Also covers `useActionState` for form errors and
session rotation in `proxy.ts`.
Three rules once `cacheComponents` is on:
- A session read is dynamic, because it reads a cookie. Put it inside a
`` boundary and the rest of the page still prerenders.
- Never read a session inside `use cache`. Runtime APIs are rejected there, and
whatever it renders is shared between visitors.
- `export const dynamic = "force-dynamic"` is no longer allowed, and no longer
needed. The `` boundary is what marks the dynamic part.
The session belongs next to the data it protects: read it in the Server
Component, Server Action or Route Handler that needs it. A layout does not
protect the pages under it, and neither does a redirect in `proxy.ts`.
## Runtimes
`getIronSession(req, res, options)` covers Node, Express, Connect and Next.js
API routes, and `getIronSession(await cookies(), options)` covers the Next.js App
Router. When you want to be explicit, or when your framework hands you something
else, pass an adapter instead:
| Adapter | For |
| ---------------------------------------- | -------------------------------------------------------------------------- |
| `nodeCookies(req, res)` | Node `http`, Express, Connect, Next.js API routes |
| `webCookies(request, responseOrHeaders)` | Anything web-standard: Hono, Bun, Deno, Cloudflare Workers, Route Handlers |
| `nextProxyCookies(request, response)` | Next.js Proxy (middleware), `proxy.ts` |
Anything with `get(name)` and `set(name, value, options)`, like Next's
`cookies()`, can be passed directly. If your framework has neither, a cookie jar
is two functions:
```ts
const session = await getIronSession(
{
read: (name) => myFramework.getCookie(name),
write: (name, value, options) => myFramework.setCookie(name, value, options),
},
options,
);
```
## Session size
A browser refuses a cookie over 4096 bytes, and iron-session throws rather than
letting one be silently dropped. Encryption adds overhead, so plan for roughly
3KB of actual data.
If you need more, `chunk: true` splits the session across several cookies. Before
you reach for it, know what the real limit is: every cookie is sent on **every
request**, and proxies cap the whole `Cookie` header well below what a few
chunks produce. nginx allows 8KB by default and a CDN in front of it may allow
less. Going over returns a 400 or 431 at the edge, before your code runs.
iron-session refuses more than 4 chunks for that reason.
The scalable answer is to keep an id in the session and the data in your
database:
```ts
session.userId = user.id; // small, stateless
const user = await db.user.findUnique({ where: { id: session.userId } });
```
## Watching for unreadable cookies
When a cookie cannot be read, iron-session starts a new empty session instead of
throwing. It has to: it cannot tell a tampered cookie from a password you
rotated out or a seal that simply expired, and a 500 on every request would be
worse. That makes real problems invisible, so log them:
```ts
const options = {
cookieName: "session",
password: process.env.SESSION_PASSWORD,
onUnsealError: (reason, error) => {
// "expired" is normal, that is how sessions end.
if (reason !== "expired") {
logger.warn({ reason, error }, "session cookie rejected");
}
},
};
```
A burst of `"unknown-password"` usually means a password rotation went wrong. A
burst of `"invalid"` can mean someone is probing your cookies.
## Validating session data
There is no `validate` option, on purpose. If you change the shape of your
session, old cookies still decrypt into the old shape, and the place to handle
that is the wrapper you already have:
```ts
// lib/session.ts
export async function getSession() {
const session = await getIronSession(await cookies(), options);
if (session.user && !SessionSchema.safeParse({ ...session }).success) {
session.destroy();
}
return session;
}
```
## Project status
✅ Production ready and maintained.
## Session options
Two options are required: `password` and `cookieName`. Everything else is automatically computed and usually doesn't need to be changed.
- `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use to generate strong passwords. `password` can be either a `string` or an `object` with incrementing keys like this: `{2: "...", 1: "..."}` to allow for password rotation. iron-session will use the highest numbered