NextjsSessions & Cookies

Sessions & Cookies

Cookies are the usual vehicle for keeping a user "logged in" between requests — the server sets one after a successful login, and the browser sends it back automatically on every subsequent request. The App Router exposes cookies through the cookies() function from next/headers.
Reading a session cookie

A Server Component reading a session cookie

TSX
import { cookies } from 'next/headers'

export default function DashboardPage() {
  const sessionToken = cookies().get('session')?.value

  if (!sessionToken) {
    return <p>Not logged in.</p>
  }

  return <p>Session token present — look it up to load the user.</p>
}
Cookies can only be SET from a Server Action or Route Handler
cookies().get(...) works anywhere in a Server Component's render — reading is always fine. But cookies().set(...) can only be called from a Server Action or a Route Handler, never during the plain render of a Server Component or layout. Setting a cookie is a side effect on the outgoing HTTP response, and a component's render function has no well-defined moment at which that response is still mutable — calling .set() there throws at runtime.

app/actions.ts — setting a cookie from a Server Action

TSX
'use server'

import { cookies } from 'next/headers'

export async function login(formData: FormData) {
  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const sessionToken = await verifyCredentials(email, password)

  cookies().set('session', sessionToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  })
}
Two session strategies

Strategy

What the cookie holds

Pros

Cons

Stateless JWT

A signed, self-contained token encoding the user ID and claims

No database lookup needed to validate a request; scales easily across servers

Hard to revoke a single session early; token grows with the data it carries

Server-side session store

Just an opaque session ID; the actual session data lives in a database or Redis

Instantly revocable (delete the row); session data can be updated without reissuing a token

Every authenticated request needs a lookup against the store

Note
Auth.js supports both strategies out of the box (strategy: 'jwt' vs. strategy: 'database'), which is one of the more consequential settings to get right for a given app — see the next page for how it fits together.
A worked example: reading and validating a session

lib/session.ts

TSX
import { cookies } from 'next/headers'

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

  // Stateless JWT: verify + decode.
  // Server-side store: look the ID up against the database instead.
  try {
    return await verifySessionToken(token)
  } catch {
    return null
  }
}

app/dashboard/page.tsx

TSX
import { getSession } from '@/lib/session'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const session = await getSession()
  if (!session) redirect('/login')

  return <p>Welcome back, {session.userName}.</p>
}
  • cookies() from next/headers reads and writes cookies in the App Router.

  • Reading a cookie works during a Server Component render; setting one only works in a Server Action or Route Handler.

  • A stateless JWT trades easy revocation for not needing a database lookup on every request.

  • A server-side session store trades a lookup-per-request for instant revocation and easy session updates.