Keep a user logged in in the Next.js App Router — set a secure session cookie (HttpOnly, Secure, SameSite), read, refresh, and delete it, log in with a Server Action, and protect routes with proxy.ts.
Why: you read the cookie to know who is logged in, refresh it to keep an active user signed in, and delete it on logout. All three go through the same async cookies() helper.
import { cookies } from 'next/headers'
const cookieStore = await cookies()
// read who is logged in (string | undefined)
const session = cookieStore.get('session')?.value
// refresh — push the expiry further out (only when a session exists)
if (session) {
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
})
}
// delete on logout
cookieStore.delete('session')Where: in the Next.js App Router the login form calls a Server Action — a function that runs only on the server, so password checks and cookie-setting never touch the browser. Validate the input, check the credentials (against a hash, e.g. with bcrypt), create the session, then redirect.
'use server'
import { redirect } from 'next/navigation'
import { createSession } from '@/app/lib/session'
export async function login(prevState: unknown, formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
// 1. look up the user and compare the hashed password (your own DB lookup)
const user = await findUser(email, password)
if (!user) return { error: 'Invalid email or password' }
// 2. create the session cookie, then send them in
await createSession(user.id)
redirect('/dashboard')
}Note: there are two layers, and you need both. A quick "optimistic" gate can redirect unauthenticated users in proxy.ts (this Next.js version uses a Proxy file — the successor to middleware.ts) — but because it runs on every route it should only read the cookie, never hit the database. The real guard belongs close to your data: re-check the session before returning anything sensitive. Hiding a button in the UI is not security.
// proxy.ts — optimistic gate: cookie only, no DB calls
import { NextResponse, type NextRequest } from 'next/server'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
const protectedRoutes = ['/dashboard']
export default async function proxy(req: NextRequest) {
const path = req.nextUrl.pathname
const session = await decrypt((await cookies()).get('session')?.value)
if (protectedRoutes.includes(path) && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
return NextResponse.next()
}