skills.vishalvoid
all skills
Architecture

Auth Session Pattern

A cookie-based session architecture that stores session data in your database, not in a JWT. Sessions are revokable, auditable, and never expose sensitive data to the client. Integrates with Next.js middleware for zero-latency auth checks on every route.

AuthNext.jsMiddlewareSecurityTypeScript

Install

$curl -fsSL https://skills.vishalvoid.com/install auth-session-pattern | bash

Why not JWTs

JWTs are stateless — you cannot invalidate a specific token without a blocklist, which negates the statelessness benefit. A database-backed session is slightly slower (one extra DB read per request) but gives you: instant logout that actually works, per-session revocation (e.g. 'log out all devices'), full audit log of session activity, and no risk of leaking sensitive data in a decodable token.

Database schema

Two tables: `User` (your user record) and `Session` (one row per active login). The session stores a cryptographically secure token, the user it belongs to, and an expiry. The token in the cookie is just a lookup key — no data lives in the cookie itself.

prisma/schema.prisma
// prisma/schema.prisma
model User {
  id           String    @id @default(cuid())
  email        String    @unique
  passwordHash String
  name         String?
  createdAt    DateTime  @default(now())
  sessions     Session[]
}

model Session {
  id        String   @id @default(cuid())
  token     String   @unique       // stored in the cookie
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  expiresAt DateTime
  createdAt DateTime @default(now())
  userAgent String?                // for audit display
  ipAddress String?

  @@index([token])
  @@index([userId])
}

Creating a session

On login, verify credentials, generate a secure random token, insert a session row, and set an `HttpOnly` cookie. The cookie carries only the token — nothing else.

lib/auth/session.ts
// lib/auth/session.ts
import { db } from '@/lib/db'
import { cookies } from 'next/headers'

const SESSION_COOKIE = 'sid'
const SESSION_DURATION_DAYS = 30

export async function createSession(userId: string, meta?: { userAgent?: string; ipAddress?: string }) {
  const token = crypto.randomUUID() + crypto.randomUUID() // 72-char opaque token

  const session = await db.session.create({
    data: {
      token,
      userId,
      expiresAt: new Date(Date.now() + SESSION_DURATION_DAYS * 86_400_000),
      userAgent: meta?.userAgent,
      ipAddress: meta?.ipAddress,
    },
  })

  const cookieStore = await cookies()
  cookieStore.set(SESSION_COOKIE, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    expires: session.expiresAt,
    path: '/',
  })

  return session
}

Reading the session

The `auth()` function is called at the top of any server component, action, or route handler that needs the current user. It reads the cookie, looks up the session, and returns the user — or `null` if there's no valid session.

lib/auth/index.ts
// lib/auth/index.ts
import { db } from '@/lib/db'
import { cookies } from 'next/headers'
import { cache } from 'react'

export type Session = {
  user: { id: string; email: string; name: string | null }
  sessionId: string
}

// cache() memoizes per-request — safe to call auth() multiple times
export const auth = cache(async (): Promise<Session | null> => {
  const cookieStore = await cookies()
  const token = cookieStore.get('sid')?.value

  if (!token) return null

  const session = await db.session.findUnique({
    where: {
      token,
      expiresAt: { gt: new Date() },   // ignore expired sessions
    },
    select: {
      id: true,
      user: {
        select: { id: true, email: true, name: true },
      },
    },
  })

  if (!session) return null

  return { user: session.user, sessionId: session.id }
})

Middleware for route protection

Next.js middleware runs on the edge before the page renders. Read the session token from the cookie and redirect unauthenticated requests to `/login`. Because this runs before the page component, there's no flash of unauthorized content.

middleware.ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const PUBLIC_PATHS = ['/', '/login', '/register', '/api/auth']

export function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl
  const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p))

  if (isPublic) return NextResponse.next()

  const token = req.cookies.get('sid')?.value

  if (!token) {
    const loginUrl = new URL('/login', req.url)
    loginUrl.searchParams.set('redirect', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Session revocation

Logout deletes the session row from the database — the token is immediately invalid everywhere, including other devices. For 'log out all devices', delete every session where `userId = currentUser.id`.

lib/auth/session.ts
// lib/auth/session.ts (continued)
export async function deleteSession(token: string) {
  await db.session.delete({ where: { token } }).catch(() => null)

  const cookieStore = await cookies()
  cookieStore.delete('sid')
}

export async function deleteAllSessions(userId: string) {
  await db.session.deleteMany({ where: { userId } })

  const cookieStore = await cookies()
  cookieStore.delete('sid')
}

Related skills

back to all skills