iron-session

Rolling Your Own Sessions with iron-session

iron-session 4,143 (https://github.com/vvo/iron-session 4,143 ) (9.0.1, MIT) is the smallest honest answer to "I just need a signed-in user." It seals an object into a cookie with iron-webcrypto, a Web Crypto port of Hapi 669,369 's iron format: PBKDF2 derives keys from your password, AES-256-CBC encrypts the payload and HMAC-SHA-256 authenticates it. No database, no table, no adapter — just a password of 32 characters or more in the environment.

One module that owns the session (lib/session.ts)TypeScript
import "server-only";   // also: cache (react), cookies (next/headers), getIronSession
export type SessionData = { userId?: string; role?: "reader" | "admin" };
export const sessionOptions: SessionOptions = {
  password: process.env.SESSION_SECRET as string,
  cookieName: "bookshelf_session",
  cookieOptions: { /* the five attributes from Section 5.9.2 */ },
};
export const getSession = async () =>
  getIronSession<SessionData>(await cookies(), sessionOptions);
export const verifySession = cache(async () => {
  const session = await getSession();
  if (!session.userId) redirect("/login");
  return { userId: session.userId, role: session.role ?? "reader" };
});

getIronSession takes the cookie store, not a request and a response, so the same call reads the session in a Server Action, a route handler and the proxy alike; writing from the proxy needs its nextProxyCookies adapter. import "server-only" makes the build fail rather than leak this module into a client bundle (Server-Only Code), and React 7,897 's cache() collapses repeated calls within one render pass into one.

Login as a Server Action (app/actions.ts)TypeScript
"use server";
export async function login(_prev: unknown, formData: FormData) {
  const user = await findUser(String(formData.get("name")));
  if (!user || !(await verifyPassword(formData, user))) {
    return { error: "Wrong user name or password." };
  }
  const session = await getSession();
  Object.assign(session, { userId: user.name, role: user.role });
  await session.save();
  redirect("/dashboard");
}

verifyPassword is bcrypt or Argon2 exactly as in Registration and Login, never a string comparison. Bind the form with useActionState (Form State and Pending UI) and the error renders under the fields while the form still posts without JavaScript; logging out is session.destroy() and a redirect.

What it leaves out is everything else: no password reset, no social sign-in, and no way to revoke a single session, because nothing on the server remembers them. If "sign out of all devices" is on your list, store a session row and keep only its id in the cookie (Session Stores for Production).