Skip to content

Session Management

A session represents a logged-in user's ongoing relationship with your app, from login until logout or expiration. This lesson covers creating, reading, refreshing, and ending sessions in a Next.js app.

The Session Lifecycle

A session begins when a user successfully authenticates and your server issues a cookie identifying them, continues as that cookie is sent with every subsequent request and validated, and ends either explicitly (logout) or implicitly (expiration, or the server invalidating it).

How exactly a session is validated depends on your strategy: database-backed sessions look up a session record on each request (easy to revoke, requires a lookup), while JWT-based sessions verify a signature locally (fast, harder to revoke before expiration).

// Reading the session on every protected request
import { cookies } from "next/headers";

export async function getSession() {
  const token = cookies().get("session")?.value;
  if (!token) return null;

  try {
    return verifySessionToken(token); // returns decoded payload or throws
  } catch {
    return null; // invalid or expired
  }
}

Centralizing this logic in one getSession() helper avoids duplicating cookie-reading and verification logic across many routes.

Common Session Cookie Settings

cookies().set("session", token, {
  httpOnly: true,     // not readable by client-side JS
  secure: true,       // only sent over HTTPS
  sameSite: "lax",    // baseline CSRF protection
  maxAge: 60 * 60 * 24 * 7, // 7 days, in seconds
  path: "/",
});
  • httpOnly and secure are baseline security settings you should almost always include.
  • maxAge controls how long the cookie persists before the browser discards it automatically.
  • sameSite: "lax" (or "strict") reduces CSRF risk by limiting cross-site cookie sending.
  • Logging out means deleting/expiring this cookie, typically via cookies().delete("session").

Session Management Cheat Sheet

Key session lifecycle events and how to handle them.

Event Typical Handling
Login Set an httpOnly session cookie
Authenticated request Read and verify the cookie's session/token
Session expiring soon Refresh token flow, or force re-login
Logout Delete the session cookie (and server-side record, if any)
Suspicious activity Invalidate all sessions for that user (requires server-side tracking)

Refresh Tokens for Longer-Lived Sessions

A common pattern combines a short-lived access token (say, 15 minutes) with a longer-lived refresh token, stored separately, used only to obtain a new access token. This limits how long a stolen access token remains useful, while still avoiding forcing users to log in again every 15 minutes.

Implementing Logout

Logging out means removing the session cookie, and, for database-backed sessions, also deleting or invalidating the corresponding server-side record so the (now-stale) cookie value can't be reused even if intercepted.

"use server";
import { cookies } from "next/headers";

export async function logout() {
  cookies().delete("session");
  redirect("/login");
}

Common Mistakes

  • Only deleting the cookie on logout without also invalidating a corresponding server-side session record.
  • Setting session cookies to never expire, increasing the impact of a stolen token.
  • Not distinguishing between "session expired" and "invalid session" when deciding how to respond to the user.
  • Forgetting sameSite and secure cookie attributes, weakening baseline security.

Key Takeaways

  • A session's lifecycle runs from login through to logout or expiration.
  • Database-backed sessions are easy to revoke; JWT-based sessions verify locally but are harder to revoke early.
  • httpOnly, secure, and sameSite are baseline cookie settings for session security.
  • Refresh tokens balance security (short-lived access tokens) with user convenience (fewer logins).
  • Logout should clear both the cookie and any corresponding server-side session record.

Pro Tip

Centralize session reading and verification into a single shared helper function used everywhere a session is needed — this makes it much easier to change your session strategy later (e.g. switching from JWTs to database sessions) without hunting down scattered, duplicated cookie-parsing logic.