Library / SDK
vvo/iron-session avatar
vvo/iron-session

iron-session v9 rejects Date objects, and its lint script builds the package twice

đź›  Secure, stateless, and cookie-based session library for Next.js or any JavaScript framework

4,141 stars254 forksTypeScriptMIT

At a glance

What is it?
iron-session keeps session state inside signed and encrypted cookies with no server-side store behind it. Version 9 tightens that contract in three places at once: timestamps have to be numbers, reads are typed as Partial, and Node 22.13 with ESM becomes the floor.
Who is it for?
iron-session fits one narrow job well: carrying a session through a signed and encrypted cookie with nothing to run on the server. Take it if you are on Node 22.13 or later and can accept that invalidating a session means changing what its cookie holds rather than deleting a row.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

v9 refuses to seal a Date object and names the field it choked on

Putting a Date into the session used to work right up until you read it back, because v8 turned that Date into a string while sealing. The type you wrote was not the type you got. v9 closes the gap by throwing, and the error names the field, so the failure lands on the assignment instead of three handlers later. The upgrade guide reduces the fix to a one-line diff:

diff
- session.lastSeen = new Date();
+ session.lastSeen = Date.now();

Milliseconds since the epoch survive the round trip as a number, which is what the sealing step needs to preserve what you wrote. Nothing else in that guide is described as required, and both call shapes keep working afterwards: `getIronSession(req, res, options)` for the Express-style signature and `getIronSession(await cookies(), options)` for the App Router one. If you had `as any` sitting on `await cookies()`, the guide tells you to delete it.

Every read is typed as Partial, so a first visit returns an empty object

The second required change is about a session that does not exist yet. A first-time visitor, a request whose cookie has expired, and a call to `destroy()` all leave you an empty object, while v8 typed reads as though your fields were guaranteed to be there. That is what let this compile and then throw at runtime:

diff
- const userId = session.user.id;
+ const userId = session.user?.id;

Reads are typed as `Partial<T>` in v9, which makes the type agree with those three cases. The cost lands on the reader instead: every field access needs a guard or an assertion, and a session that should have been logged in now looks exactly like one that was never created. The guide calls this one of the two things v8 got wrong quietly, and sends you to MIGRATION.md for the full list of removed APIs.

v9 reads v8 cookies and v8 reads v9 cookies, but older ones are signed out once

Reverting a deploy does not sign everyone out, which is not what you expect from a library that just changed its cookie format. Compatibility holds in both directions: v9 reads v8 cookies and v8 reads v9 cookies. A release that ships v9 on a Tuesday afternoon and gets rolled back that evening leaves the sessions already in browsers intact on either side.

There is one exception, and it is deliberate. MIGRATION.md is described as carrying the removed APIs plus a security fix that signs pre-v8 cookies out once. Cookies created before v8 do not survive that upgrade, while v8 cookies do, so the re-login wave is bounded by how old your install was rather than by the format change itself.

The same upgrade note sits at the top of the README as an important callout for anyone arriving from v8, with the short version up front and the migration guide behind it.

ESM-only, with the one attw rule the project switches off on purpose

The manifest declares the module type and a single export mapping, `.` to `./dist/index.js`, with types at `./dist/index.d.ts` and `sideEffects` set to false. Installing it is one command:

sh
pnpm add iron-session

v9 needs Node 22.13 or later and is ESM-only, yet `require()` still works there, because Node 22.13 can require an ES module. The repository states that trade-off in its own pipeline: the type-checking step for published packages carries an ignore flag for the rule that fires when a CommonJS consumer resolves to ESM. That complaint is suppressed rather than fixed, which is the honest outcome for a package that keeps the require path working through Node itself.

Anyone stuck on an older Node is told to stay on v8 with `pnpm add iron-session@8`. The published tarball is also narrower than the repository: the files array lists `dist/*` and `MIGRATION.md` and nothing else, which is why shipping the migration guide matters when the README links to it from the upgrade note. There is no subpath export, so the session functions and the cookie adapters all arrive through one entry point.

Middleware needs an adapter, because a raw Set-Cookie header has no effect

Next.js merges a cookie into the current render only when it goes through `response.cookies.set()`. Write a raw `Set-Cookie` header in middleware instead and it looks like it works, then has no effect, which is the whole reason the library ships an adapter for that runtime. In current Next.js the middleware file is `proxy.ts`, and the README example saves inside it:

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;
}

The adapter takes the request and the response together, which is what the plain two-argument signature cannot express for a middleware handler. The table of contents lists `nodeCookies` and `webCookies` beside it and anchors all three adapters under its Runtimes entry, so picking the wrong one is a naming decision rather than a runtime error.

The lint script builds the package twice and packs it twice more

Linting is a chain rather than one command:

sh
oxlint --type-aware && tsc --noEmit && pnpm build && publint && attw --pack . --ignore-rules cjs-resolves-to-esm && pnpm lint:types

The final step starts the bundler again and then type-checks a second project with `tsc --noEmit -p type-tests/tsconfig.json`. So one lint run builds the bundle twice and packs the tarball twice, once for the package linter and once for the types checker.

The configuration does not line up with those scripts. The tree carries `.eslintrc.yaml` and `.oxlintrc.json` side by side, plus `.prettierignore` and `.editorconfig`, while every script calls `oxlint` and `oxfmt` and none calls eslint or prettier. Two linters and two formatters are configured, and one pair is wired in.

Unit tests run on Node's own test runner with coverage and tsx as the loader, pointed at `src/*.test.ts`, so the test files live next to the source instead of in a separate directory, and a pretest step creates the coverage directory before the run. End-to-end work goes through Playwright twice, once against the local setup and once through `playwright.deployed.config.ts` against the hosted demo.

Three v9 tags in eighty minutes, and a manifest that keeps up

The v9 line landed in a single afternoon. v9.0.0-beta.1 was published at 20:43 UTC on 2026-08-30, v9.0.0 at 20:56, and v9.0.1 at 22:01, all on that day. That is a narrow window for a release that moves the cookie format, the Node floor and the read types at once, and it is worth reading as the project's own view of how contained the change was.

What the repository does get right is the version field. The manifest reads 9.0.1, matching the newest tag, so there is no drift between package.json and the release line. The last push on record is 2026-09-23 and the repository is not archived, which puts the tree three weeks past its newest tag rather than at rest.

Both ends of the release process are automated: `.release-it.yaml` handles tagging and publishing, and `renovate.json` handles dependency updates. The publish config enables npm provenance and pins the public registry, and the manifest names two funding links, one for the author and one for a second account.

The README's examples stop mid-sentence and the table of contents promises the rest

The examples section names its first pattern, Server Components and Server Actions, links to the source under `examples/next/`, and then stops partway through its own sentence at `the action writ`. Every entry the table of contents lists after that point has no text in the file: the second example, Runtimes, Session size, Watching for unreadable cookies, Validating session data, Project status, Session options, the API reference and the FAQ.

The table of contents is still the best inventory of the surface. It names two `getIronSession` overloads, the three cookie adapters, `session.save()`, `session.destroy()`, `session.updateConfig()`, and the standalone `sealData` and `unsealData` functions, each of which takes a password and a ttl. Those last two are the path for code with no request or response object to pass, and the ttl is what lets a cookie expire on its own.

The upgrade section also names three options worth taking during a migration rather than after it: `nextProxyCookies`, `onUnsealError` for finding out why a cookie was rejected instead of guessing, and `chunk: true` for a session that has outgrown a single cookie.

Editorial conclusion

iron-session fits one narrow job well: carrying a session through a signed and encrypted cookie with nothing to run on the server. Take it if you are on Node 22.13 or later and can accept that invalidating a session means changing what its cookie holds rather than deleting a row. Before upgrading, fix the two v9 breaks, because v9 turns a silent type lie into a thrown error and signs out cookies older than v8 once. If you need revocation on demand, a CommonJS build, or an older Node, stay on v8 or choose something with a store.

Frequently asked questions

What is iron-session?

It is a stateless session library for JavaScript that keeps session data in signed and encrypted cookies, which your server code decodes with no network call involved, the same technique the README credits to Ruby on Rails. It is aimed at Next.js and at plain Node HTTP stacks, and its table of contents names a `destroy()` method and an `updateConfig()` method on the session object.

What is iron-session in Next.js?

You reach it through one function, `getIronSession`, with a different shape per runtime: `getIronSession(req, res, options)` for API routes and Express-style handlers, `getIronSession(await cookies(), options)` for App Router route handlers, Server Components and Server Actions, and the `nextProxyCookies` adapter inside `proxy.ts`, which was `middleware.ts` before Next 16.

Is iron-session secure?

Session data is sealed into signed and encrypted cookies using the password you pass as a session option, so your server decrypts it locally and there is no session store behind it to query or leak. The README also points at a security fix in MIGRATION.md that signs pre-v8 cookies out once, so cookies created before v8 do not survive an upgrade.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. vvo/iron-session on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vvo-iron-session.svg)](https://hysenlabs.com/projects/vvo-iron-session)