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.
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.
"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).