Authentication and sessions
DB sessions vs JWTs, where to keep the token, the DAL and auth libraries.
Updated
What authentication, authorization and a session are
Three different concepts that often get mixed up:
| Concept | The question | Example |
|---|---|---|
| Authentication | Who are you? | logging in with email + password, Google, a magic link |
| Session | How do I recognize you on the next request? | a cookie sent automatically with every request |
| Authorization | Are you allowed to do this? | only the author can delete their post |
How they work together: you authenticate once → the server creates a session and sets a cookie → on every request, the server reads the cookie, finds out who you are and checks whether you're allowed (authorization).
Two kinds of sessions
A session in the database (stateful)
The cookie holds only a random id. The session data (the user, the expiry) lives on the server, in a sessions table or in Redis.
A session in the cookie (stateless, JWT)
The cookie holds the data itself (userId, expiry), signed (or encrypted) with a secret key on the server. The server checks the signature — without querying the database. The commonly used format: JWT.
| Database session | Stateless (a JWT in a cookie) | |
|---|---|---|
| What's in the cookie | a random id | the data, signed |
| A DB query on every request | yes (or a cache) | no |
| Instant logout / revocation | yes — you delete the row | hard — the token is valid until it expires |
| "Log out of all devices" | easy | hard |
| Cookie size | small | bigger |
| When | apps with sensitive data, fine-grained control | simple apps, many servers without a shared DB |
Where to keep the token — the important rule
| Place | Verdict |
|---|---|
an HttpOnly; Secure; SameSite=Lax cookie |
✓ correct — JS can't read it, so neither can an injected script (XSS) |
localStorage / sessionStorage |
✗ any script on the page can steal it |
| a variable in memory (an SPA) | possible, but it's lost on reload; needs a refresh token in a cookie |
Access tokens and refresh tokens
For APIs, two tokens are often used:
| Access token | Refresh token | |
|---|---|---|
| Lifetime | short (minutes) | long (days, weeks) |
| Used on | every request | only to get a new access token |
| If stolen | damage limited in time | can be revoked on the server |
Sessions in Next.js — the pieces
// shared/api/session.ts
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'
const key = new TextEncoder().encode(process.env.SESSION_SECRET)
export async function createSession(userId: string) {
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const token = await new SignJWT({ userId }).setProtectedHeader({ alg: 'HS256' }).setExpirationTime(expires).sign(key)
;(await cookies()).set('session', token, { httpOnly: true, secure: true, sameSite: 'lax', expires, path: '/' })
}
export async function readSession() {
const token = (await cookies()).get('session')?.value
if (!token) return null
try {
return (await jwtVerify(token, key, { algorithms: ['HS256'] })).payload as { userId: string }
} catch {
return null
}
}Where you check — the layers
| Layer | Role | Enough on its own? |
|---|---|---|
proxy.ts |
an optimistic check: "is there a cookie?" → redirect to /login |
no — just UX, fast |
the Data Access Layer (verifySession) |
really checks the session, once per request (cache()) |
yes — this is where the security is |
| every Server Action / Route Handler | authentication + authorization on the specific resource | mandatory |
| the UI (hiding buttons) | experience only | never |
// shared/api/dal.ts
import 'server-only'
import { cache } from 'react'
import { redirect } from 'next/navigation'
export const verifySession = cache(async () => {
const session = await readSession()
if (!session) redirect('/login')
return session
})Layouts don't re-check on every navigation (they're kept). Put the check in the page, in the DAL or in the action — not only in the layout.
Libraries — don't write authentication yourself
| Library | Kind | When |
|---|---|---|
| Auth.js (NextAuth) | open source, in your app | OAuth (Google, GitHub), DB or JWT sessions |
| Better Auth | open source, in your app | email/password, OAuth, 2FA, organizations — complete and typed |
| Clerk, Auth0, WorkOS, Stack Auth | a hosted service | ready-made UI, enterprise (SSO), no maintenance |
| iron-session, jose | sessions only | you already have the login logic, you just need the cookie |
Authentication has many security details (password hashing, rate limiting, password resets, CSRF). A mature library is almost always the right choice.
Summary
- Authentication = who you are; a session = how you're recognized; authorization = what you're allowed to do.
- A DB session = easy revocation; a JWT in a cookie = no query, hard revocation.
- The token lives in an
HttpOnlycookie; you check in the DAL and in every action, the proxy is only optimistic.