[{"data":1,"prerenderedAt":345},["ShallowReactive",2],{"blog-how-to/session-management":3},{"id":4,"title":5,"body":6,"category":326,"date":327,"dateModified":327,"description":328,"draft":329,"extension":330,"faq":331,"featured":329,"headerVariant":332,"image":331,"keywords":331,"meta":333,"navigation":334,"ogDescription":335,"ogTitle":331,"path":336,"readTime":331,"schemaOrg":337,"schemaType":338,"seo":339,"sitemap":340,"stem":341,"tags":342,"twitterCard":343,"__hash__":344},"blog/blog/how-to/session-management.md","How to Implement Secure Session Management",{"type":7,"value":8,"toc":310},"minimark",[9,13,17,21,27,30,43,48,51,55,75,88,101,114,127,140,153,185,189,217,221,226,229,233,236,240,243,247,250,272,291],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-implement-secure-session-management",[18,19,20],"p",{},"Server-side sessions done right",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (25 minutes):\nGenerate 32+ byte random session IDs, store hashed IDs in your database with user info and expiry, use httpOnly/secure/sameSite cookies, regenerate session ID on login/privilege change, implement sliding expiration (extend on activity), and allow users to view and revoke their sessions.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"Database or Redis for session storage",[34,38,39],{},"Basic understanding of cookies",[34,41,42],{},"Authentication system to protect",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"Sessions are how your app \"remembers\" logged-in users. Weak session management leads to session hijacking, fixation attacks, and account takeovers. Server-side sessions are generally more secure than JWTs because you can instantly revoke them.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"generate-secure-session-ids","Generate secure session IDs",[65,66,71],"pre",{"className":67,"code":69,"language":70},[68],"language-text","import crypto from 'crypto';\n\n// Generate cryptographically secure session ID\nfunction generateSessionId(): string {\n  // 32 bytes = 256 bits of entropy\n  return crypto.randomBytes(32).toString('hex');\n}\n\n// Hash the session ID before storing\nfunction hashSessionId(sessionId: string): string {\n  return crypto.createHash('sha256').update(sessionId).digest('hex');\n}\n\n// NEVER do this:\n// const sessionId = Date.now().toString();  // Predictable!\n// const sessionId = Math.random().toString(); // Not cryptographically secure!\n","text",[72,73,69],"code",{"__ignoreMap":74},"",[56,76,78,82],{"number":77},"2",[60,79,81],{"id":80},"create-session-storage-schema","Create session storage schema",[65,83,86],{"className":84,"code":85,"language":70},[68],"// Prisma schema\nmodel Session {\n  id           String   @id @default(cuid())\n  sessionHash  String   @unique  // Store hashed session ID\n  userId       String\n  user         User     @relation(fields: [userId], references: [id])\n  userAgent    String?\n  ipAddress    String?\n  createdAt    DateTime @default(now())\n  expiresAt    DateTime\n  lastActiveAt DateTime @default(now())\n\n  @@index([userId])\n  @@index([expiresAt])\n}\n\n// SQL equivalent\nCREATE TABLE sessions (\n  id VARCHAR(36) PRIMARY KEY,\n  session_hash VARCHAR(64) UNIQUE NOT NULL,\n  user_id VARCHAR(36) NOT NULL REFERENCES users(id),\n  user_agent TEXT,\n  ip_address VARCHAR(45),\n  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n  expires_at TIMESTAMP NOT NULL,\n  last_active_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n  INDEX idx_user_id (user_id),\n  INDEX idx_expires_at (expires_at)\n);\n",[72,87,85],{"__ignoreMap":74},[56,89,91,95],{"number":90},"3",[60,92,94],{"id":93},"create-and-store-sessions","Create and store sessions",[65,96,99],{"className":97,"code":98,"language":70},[68],"import { addDays } from 'date-fns';\n\ninterface CreateSessionOptions {\n  userId: string;\n  userAgent?: string;\n  ipAddress?: string;\n  rememberMe?: boolean;\n}\n\nasync function createSession(options: CreateSessionOptions) {\n  const sessionId = generateSessionId();\n  const sessionHash = hashSessionId(sessionId);\n\n  const expiresAt = options.rememberMe\n    ? addDays(new Date(), 30)  // 30 days for \"remember me\"\n    : addDays(new Date(), 7);   // 7 days default\n\n  await prisma.session.create({\n    data: {\n      sessionHash,\n      userId: options.userId,\n      userAgent: options.userAgent,\n      ipAddress: options.ipAddress,\n      expiresAt\n    }\n  });\n\n  // Return the unhashed session ID for the cookie\n  return { sessionId, expiresAt };\n}\n\n// Set the session cookie\nfunction setSessionCookie(res: Response, sessionId: string, expiresAt: Date) {\n  res.cookie('sessionId', sessionId, {\n    httpOnly: true,       // Can't be accessed by JavaScript\n    secure: process.env.NODE_ENV === 'production',  // HTTPS only in prod\n    sameSite: 'lax',      // Protects against CSRF\n    expires: expiresAt,\n    path: '/'\n  });\n}\n",[72,100,98],{"__ignoreMap":74},[56,102,104,108],{"number":103},"4",[60,105,107],{"id":106},"validate-sessions","Validate sessions",[65,109,112],{"className":110,"code":111,"language":70},[68],"interface ValidatedSession {\n  user: User;\n  sessionId: string;\n}\n\nasync function validateSession(sessionId: string): Promise {\n  if (!sessionId) return null;\n\n  const sessionHash = hashSessionId(sessionId);\n\n  const session = await prisma.session.findUnique({\n    where: { sessionHash },\n    include: { user: true }\n  });\n\n  // Session not found\n  if (!session) return null;\n\n  // Session expired\n  if (session.expiresAt \u003C new Date()) {\n    await prisma.session.delete({ where: { id: session.id } });\n    return null;\n  }\n\n  // Update last active time (sliding expiration)\n  await prisma.session.update({\n    where: { id: session.id },\n    data: {\n      lastActiveAt: new Date(),\n      // Optionally extend expiration on activity\n      expiresAt: addDays(new Date(), 7)\n    }\n  });\n\n  return { user: session.user, sessionId };\n}\n\n// Auth middleware\nasync function authMiddleware(req: Request, res: Response, next: NextFunction) {\n  const sessionId = req.cookies.sessionId;\n  const session = await validateSession(sessionId);\n\n  if (!session) {\n    res.clearCookie('sessionId');\n    return res.status(401).json({ error: 'Not authenticated' });\n  }\n\n  req.user = session.user;\n  req.sessionId = session.sessionId;\n  next();\n}\n",[72,113,111],{"__ignoreMap":74},[56,115,117,121],{"number":116},"5",[60,118,120],{"id":119},"regenerate-session-on-privilege-change","Regenerate session on privilege change",[65,122,125],{"className":123,"code":124,"language":70},[68],"// Critical: Always regenerate session ID on:\n// - Login\n// - Password change\n// - Role/permission change\n// - Any security-sensitive action\n\nasync function regenerateSession(req: Request, res: Response) {\n  const oldSessionId = req.cookies.sessionId;\n\n  if (oldSessionId) {\n    // Delete old session\n    const oldHash = hashSessionId(oldSessionId);\n    await prisma.session.delete({\n      where: { sessionHash: oldHash }\n    }).catch(() => {}); // Ignore if already deleted\n  }\n\n  // Create new session\n  const { sessionId, expiresAt } = await createSession({\n    userId: req.user.id,\n    userAgent: req.headers['user-agent'],\n    ipAddress: req.ip\n  });\n\n  setSessionCookie(res, sessionId, expiresAt);\n  return sessionId;\n}\n\n// Usage in login\nasync function login(req: Request, res: Response) {\n  // ... verify credentials ...\n\n  // Create session\n  const { sessionId, expiresAt } = await createSession({\n    userId: user.id,\n    userAgent: req.headers['user-agent'],\n    ipAddress: req.ip,\n    rememberMe: req.body.rememberMe\n  });\n\n  setSessionCookie(res, sessionId, expiresAt);\n  return res.json({ success: true });\n}\n\n// Usage in password change\nasync function changePassword(req: Request, res: Response) {\n  // ... update password ...\n\n  // Regenerate session (invalidates old session)\n  await regenerateSession(req, res);\n\n  // Optionally: revoke all other sessions\n  await revokeAllUserSessions(req.user.id, req.cookies.sessionId);\n\n  return res.json({ success: true });\n}\n",[72,126,124],{"__ignoreMap":74},[56,128,130,134],{"number":129},"6",[60,131,133],{"id":132},"implement-session-revocation","Implement session revocation",[65,135,138],{"className":136,"code":137,"language":70},[68],"// Logout - revoke current session\nasync function logout(req: Request, res: Response) {\n  const sessionId = req.cookies.sessionId;\n\n  if (sessionId) {\n    const sessionHash = hashSessionId(sessionId);\n    await prisma.session.delete({\n      where: { sessionHash }\n    }).catch(() => {});\n  }\n\n  res.clearCookie('sessionId');\n  return res.json({ success: true });\n}\n\n// Revoke all sessions except current\nasync function revokeAllUserSessions(userId: string, exceptSessionId?: string) {\n  const exceptHash = exceptSessionId ? hashSessionId(exceptSessionId) : null;\n\n  await prisma.session.deleteMany({\n    where: {\n      userId,\n      ...(exceptHash && { sessionHash: { not: exceptHash } })\n    }\n  });\n}\n\n// List user's active sessions\nasync function listUserSessions(userId: string) {\n  return prisma.session.findMany({\n    where: {\n      userId,\n      expiresAt: { gt: new Date() }\n    },\n    select: {\n      id: true,\n      userAgent: true,\n      ipAddress: true,\n      createdAt: true,\n      lastActiveAt: true\n    },\n    orderBy: { lastActiveAt: 'desc' }\n  });\n}\n\n// Revoke specific session\nasync function revokeSession(userId: string, sessionDbId: string) {\n  await prisma.session.deleteMany({\n    where: {\n      id: sessionDbId,\n      userId  // Ensure user can only revoke their own sessions\n    }\n  });\n}\n",[72,139,137],{"__ignoreMap":74},[56,141,143,147],{"number":142},"7",[60,144,146],{"id":145},"clean-up-expired-sessions","Clean up expired sessions",[65,148,151],{"className":149,"code":150,"language":70},[68],"// Run this periodically (cron job, scheduled function)\nasync function cleanupExpiredSessions() {\n  const result = await prisma.session.deleteMany({\n    where: {\n      expiresAt: { lt: new Date() }\n    }\n  });\n\n  console.log(`Cleaned up ${result.count} expired sessions`);\n}\n\n// For high-traffic apps, consider using Redis instead\nimport Redis from 'ioredis';\n\nconst redis = new Redis(process.env.REDIS_URL);\n\nasync function createSessionRedis(userId: string, sessionId: string, ttl: number) {\n  const sessionHash = hashSessionId(sessionId);\n  const sessionData = JSON.stringify({\n    userId,\n    createdAt: Date.now()\n  });\n\n  await redis.set(`session:${sessionHash}`, sessionData, 'EX', ttl);\n}\n\nasync function validateSessionRedis(sessionId: string) {\n  const sessionHash = hashSessionId(sessionId);\n  const data = await redis.get(`session:${sessionHash}`);\n\n  if (!data) return null;\n\n  // Extend TTL on activity\n  await redis.expire(`session:${sessionHash}`, 7 * 24 * 60 * 60);\n\n  return JSON.parse(data);\n}\n",[72,152,150],{"__ignoreMap":74},[154,155,156,159],"warning-box",{},[18,157,158],{},"Session Security Checklist:",[31,160,161,164,167,170,173,176,179,182],{},[34,162,163],{},"Use 32+ bytes of cryptographically secure randomness for session IDs",[34,165,166],{},"Store hashed session IDs, not raw values",[34,168,169],{},"Set httpOnly, secure, and sameSite on cookies",[34,171,172],{},"Regenerate session ID on login and privilege changes",[34,174,175],{},"Implement absolute and idle timeout",[34,177,178],{},"Bind sessions to user agent and/or IP (optional, can cause issues)",[34,180,181],{},"Allow users to view and revoke their sessions",[34,183,184],{},"Clean up expired sessions regularly",[44,186,188],{"id":187},"how-to-verify-it-worked","How to Verify It Worked",[190,191,192,199,205,211],"ol",{},[34,193,194,198],{},[195,196,197],"strong",{},"Check cookie attributes:"," Use browser DevTools to verify httpOnly, secure, sameSite",[34,200,201,204],{},[195,202,203],{},"Test session fixation:"," Set a session ID before login, verify it changes after login",[34,206,207,210],{},[195,208,209],{},"Test revocation:"," Logout and verify the session cookie is cleared and old session is invalid",[34,212,213,216],{},[195,214,215],{},"Test expiration:"," Create a session with short expiry, wait, verify it's rejected",[44,218,220],{"id":219},"common-errors-troubleshooting","Common Errors & Troubleshooting",[222,223,225],"h4",{"id":224},"session-not-persisting-across-requests","Session not persisting across requests",[18,227,228],{},"Check cookie domain, path, and secure settings. In development without HTTPS, set secure: false.",[222,230,232],{"id":231},"session-immediately-expires","Session immediately expires",[18,234,235],{},"Check that expiresAt is set correctly and your server/client clocks are synchronized.",[222,237,239],{"id":238},"cant-logout-session-persists","Can't logout (session persists)",[18,241,242],{},"Ensure you're deleting the session from the database AND clearing the cookie.",[222,244,246],{"id":245},"performance-issues-with-many-sessions","Performance issues with many sessions",[18,248,249],{},"Add indexes on sessionHash, userId, and expiresAt. Consider moving to Redis for high-traffic apps.",[251,252,253,260,266],"faq-section",{},[254,255,257],"faq-item",{"question":256},"Sessions vs JWTs - which should I use?",[18,258,259],{},"Server-side sessions are generally better for web apps: instant revocation, smaller cookies, no token expiration complexity. JWTs are better for stateless APIs, microservices, or mobile apps where you need offline token validation.",[254,261,263],{"question":262},"Should I bind sessions to IP address?",[18,264,265],{},"It adds security but can break for users on mobile networks or with changing IPs. Consider logging IP changes and notifying users rather than immediately invalidating sessions.",[254,267,269],{"question":268},"How do I handle concurrent sessions?",[18,270,271],{},"Allow multiple sessions by default (users on multiple devices). Let users view and revoke sessions. Optionally limit to N concurrent sessions for sensitive apps.",[18,273,274,277,282,283,282,287],{},[195,275,276],{},"Related guides:",[278,279,281],"a",{"href":280},"/blog/how-to/secure-login-form","Secure Login Form"," ·\n",[278,284,286],{"href":285},"/blog/how-to/jwt-security","JWT Security",[278,288,290],{"href":289},"/blog/how-to/implement-csrf-protection","CSRF Protection",[292,293,294,300,305],"related-articles",{},[295,296],"related-card",{"description":297,"href":298,"title":299},"Step-by-step guide to setting up Supabase Auth securely. Configure authentication, handle sessions, integrate with RLS, ","/blog/how-to/supabase-auth","How to Set Up Supabase Auth Securely",[295,301],{"description":302,"href":303,"title":304},"Learn to write effective Row Level Security policies in Supabase. Real examples for profiles, posts, teams, and multi-te","/blog/how-to/supabase-rls-policies","How to Write Supabase RLS Policies",[295,306],{"description":307,"href":308,"title":309},"Verify your Row Level Security policies work correctly. Learn SQL testing methods, browser testing, and automated testin","/blog/how-to/test-supabase-rls","How to Test Supabase RLS Policies",{"title":74,"searchDepth":311,"depth":311,"links":312},2,[313,314,324,325],{"id":46,"depth":311,"text":47},{"id":53,"depth":311,"text":54,"children":315},[316,318,319,320,321,322,323],{"id":62,"depth":317,"text":63},3,{"id":80,"depth":317,"text":81},{"id":93,"depth":317,"text":94},{"id":106,"depth":317,"text":107},{"id":119,"depth":317,"text":120},{"id":132,"depth":317,"text":133},{"id":145,"depth":317,"text":146},{"id":187,"depth":311,"text":188},{"id":219,"depth":311,"text":220},"how-to","2026-01-26","Step-by-step guide to secure session management. Create, store, validate, and expire sessions properly to protect user accounts.",false,"md",null,"yellow",{},true,"Build secure server-side session management from scratch.","/blog/how-to/session-management","[object Object]","HowTo",{"title":5,"description":328},{"loc":336},"blog/how-to/session-management",[],"summary_large_image","7KOBYUiZLJycI-AZDuB9frf9tpzhOvt3z9zciikp9MQ",1775843927217]