getSession() fetches from the API route /api/auth/session — this GET request is cached by the Data Cache by default, returning stale session data for hours
getUser() reads the session directly from the database or JWT — no HTTP call, no caching, always fresh
The Data Cache caches GET /api/auth/session responses — the first user's session is cached and returned to ALL users
Never use getSession() in Server Components — use getUser() from auth() or read the session cookie directly
Auth.js middleware must have a matcher — without it, the auth check runs on every static file request
The fix: use auth() (which calls getUser internally) in Server Components, and getSession() only in client components where the fetch cache does not apply
✦ Definition~90s read
What is Authentication in Next.js 16?
Auth.js (formerly NextAuth.js v5) is the official authentication library for Next.js 16. It provides a flexible authentication system supporting OAuth providers (Google, GitHub, etc.), credentials (email/password), and magic links. It handles session management via JWT or database sessions, middleware-based route protection, and CSRF protection.
★
getSession() is like asking a receptionist for someone's name — the receptionist writes it down and gives the same answer to everyone who asks for the next 2 hours.
The critical architectural detail in Next.js 16 is the distinction between getSession() (HTTP fetch to route handler, subject to Data Cache) and auth() (direct session read, no cache). This distinction is invisible in documentation but causes production incidents when the Data Cache caches auth responses.
Auth.js integrates with Next.js 16 via route handlers (app/api/auth/[...nextauth]), middleware (middleware.ts), and React hooks (useSession). It supports both server-side and client-side session access patterns.
Plain-English First
getSession() is like asking a receptionist for someone's name — the receptionist writes it down and gives the same answer to everyone who asks for the next 2 hours. getUser() is like walking directly to the person's desk and reading their name tag — always accurate. The receptionist's notebook (Data Cache) is helpful for slowing down irrelevant questions but catastrophic for authentication data.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Authentication with Auth.js (NextAuth.js v5) in Next.js 16 introduces a critical distinction that most tutorials gloss over: getSession() vs getUser(). getSession() makes an HTTP request to /api/auth/session — which is a GET route handler that the Data Cache caches by default. getUser() (via the auth() helper) reads the session directly from the JWT cookie or database — no HTTP call, no caching.
This distinction caused a production incident where getSession() returned stale session data for 2 hours during peak traffic — the Data Cache cached the first user's response and served it to all subsequent users. Users saw each other's profiles, orders, and account data.
This article covers Auth.js v5 setup with Next.js 16, the getSession() vs getUser() distinction, middleware protection patterns, session management with JWT vs database sessions, provider configuration (credentials, OAuth, magic links), and the production incident that exposed the caching trap.
Tested with next-auth@5 beta (Auth.js v5) on Next.js 16.0.0-canary.
getSession() vs auth(): The Single Most Important Auth Distinction in Next.js 16
Auth.js v5 provides two methods to read the current session:
getSession(): Makes an HTTP GET request to /api/auth/session. This is an HTTP call — it goes through the network stack, hits a route handler, reads the session cookie, and returns the user data. In Next.js 15+, GET route handlers are cached by the Data Cache by default. The first response is cached and returned to all subsequent callers for the cache duration.
auth() (also available as getUser() in some Auth.js versions): Reads the session directly from the JWT cookie or database session. No HTTP call, no route handler invocation, no Data Cache involvement. It decrypts the JWT or queries the database directly. Always fresh, always per-user, never cached.
The result: getSession() in a Server Component returns a cached response that may belong to a different user. auth() always returns the correct session for the current user.
The fix is simple: never use getSession() in Server Components. Use auth() instead. getSession() is only safe in client components (useSession hook) where the fetch goes through the browser's network stack and is not subject to the Next.js Data Cache.
getSession() makes an HTTP GET to /api/auth/session. The Data Cache caches this response. The first user's session is returned to ALL users. Use auth() instead — it reads the session directly from the JWT cookie without any fetch call.
Production Insight
The 2-hour session leak incident: getSession() in a Server Component cached the first user's session. 10,000 subsequent users saw user A's profile. The team received 200+ support tickets before realizing the auth data was cached.
Rule: grep for getSession() in all Server Components and replace with auth(). Add this check to your CI.
Key Takeaway
Use auth() in Server Components and Route Handlers — reads session directly, no HTTP call, no cache, always fresh.
getSession() makes an HTTP GET request that the Data Cache caches — NEVER use in Server Components.
getSession() is safe in client components (useSession) where the browser handles the fetch.
thecodeforge.io
Nextjs Authentication Authjs Guide
Force-Dynamic: Protecting Auth Routes from the Data Cache
The auth route handler at app/api/auth/[...nextauth]/route.ts is a GET route handler. By default, it is cached by the Data Cache. This is catastrophic for authentication — the response depends on the requesting user's session cookie, but the Data Cache caches by URL, not by user.
The fix: add export const dynamic = 'force-dynamic' to the auth route handler. This tells Next.js to never cache this route — every request runs through the full handler, reads the session cookie, and returns the correct user.
Additionally, add Cache-Control headers to the auth endpoint response to prevent downstream caching (CDN, browser): - Cache-Control: no-store, no-cache, must-revalidate - Pragma: no-cache
If you use Auth.js's built-in handler wrapping, the cache control headers may already be set. But the Data Cache operates at a different layer — it caches before response headers are evaluated. force-dynamic is required regardless of response headers.
app/api/auth/[...nextauth]/route.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ============================================// Auth route handler — MUST be force-dynamic// ============================================// app/api/auth/[...nextauth]/route.tsimport { handlers } from'@/src/auth'// ❌ WITHOUT this, the Data Cache caches auth responses// The first user's session is cached and returned to everyone// ✅ REQUIRED: prevent Data Cache from caching auth responsesexportconst dynamic = 'force-dynamic'// Optional: set explicit revalidation to 0// export const revalidate = 0// Export the Auth.js handlers for GET and POSTexportconst { GET, POST } = handlers
// ============================================// Alternative: Full manual handler with cache control// ============================================// app/api/auth/[...nextauth]/route.ts (manual version)// import { handlers } from '@/src/auth'// import { NextResponse } from 'next/server'//// export const dynamic = 'force-dynamic'//// export async function GET(request: Request) {// const response = await handlers.GET(request)//// // Add cache-prevention headers// response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate')// response.headers.set('Pragma', 'no-cache')// response.headers.set('Expires', '0')//// return response// }//// export const POST = handlers.POST// ============================================// CI check: ensure auth routes are force-dynamic// ============================================// In CI pipeline:// grep -r 'force-dynamic' app/api/auth/ && echo 'AUTH ROUTE IS DYNAMIC' || echo 'WARNING: Auth route may be cached!'//// If the check fails, fail the CI build.// One line of config prevents a P0 security incident.
Every auth-related route handler MUST have export const dynamic = 'force-dynamic'. Without it, GET /api/auth/session is cached by the Data Cache — returning one user's session to all users. Add this to your CI as a mandatory check.
Production Insight
The 2-hour session leak was caused by ONE missing line: 'export const dynamic = "force-dynamic"'. Adding it after the incident fixed the cross-user data exposure. The Data Cache was the root cause, but the missing config was the trigger.
Rule: any route handler that reads auth/identity data must be force-dynamic. Add this to code review checklists.
Key Takeaway
Add 'export const dynamic = "force-dynamic"' to /api/auth/[...nextauth]/route.ts.
Without it, the Data Cache caches GET /api/auth/session — one user's session for all users.
Add CI checks to enforce force-dynamic on auth routes.
Middleware Protection: Auth Check with Proper Matcher
Auth.js middleware wraps the auth check to protect routes. The most common pattern wraps the entire middleware with the auth middleware:
export { default } from 'next-auth/middleware'
This exports the Auth.js middleware as the default middleware. It checks for a valid session on every request — if no session, redirects to /login.
PROBLEM: Without a matcher, this runs on EVERY request — CSS files, JS chunks, images, fonts, API routes. Every static asset goes through the auth middleware, checks the session cookie, and potentially redirects to login.
The fix: always add a matcher config that restricts auth checks to protected routes only. Do NOT use the broad 'exclude static assets' regex for auth middleware — you want auth to ONLY run on specific paths, not on most paths.
Use the explicit path array format: matcher: ['/dashboard/:path', '/account/:path', '/admin/:path*']. This way, auth middleware only runs on paths that need protection. All other paths (public pages, static assets, API routes that handle their own auth) bypass middleware entirely.
middleware.ts — Auth.js with proper matcherTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ============================================// Auth.js Middleware — ALWAYS with matcher// ============================================// middleware.tsimport { auth } from'@/src/auth'import { NextResponse } from'next/server'importtype { NextRequest } from'next/server'// Option 1: Simple export (if using default export from Auth.js)// export { default } from 'next-auth/middleware'// But you MUST still add matcher config below this line// Option 2: Custom middleware with auth()exportdefaultauth(asyncfunctionmiddleware(request: NextRequest) {
const session = awaitauth()
const { nextUrl } = request
// Define protected pathsconst isProtectedPath = [
'/dashboard',
'/account',
'/admin',
'/orders',
].some(path => nextUrl.pathname.startsWith(path))
if (isProtectedPath && !session?.user) {
// Redirect to login with return URLconst loginUrl = newURL('/login', nextUrl.origin)
loginUrl.searchParams.set('callbackUrl', nextUrl.pathname)
returnNextResponse.redirect(loginUrl)
}
// Redirect logged-in users away from login pageif (nextUrl.pathname === '/login' && session?.user) {
returnNextResponse.redirect(newURL('/dashboard', nextUrl.origin))
}
returnNextResponse.next()
})
// CRITICAL: Restrict middleware to specific pathsexportconst config = {
matcher: [
// Protected routes — only these run through auth middleware'/dashboard/:path*',
'/account/:path*',
'/admin/:path*',
'/orders/:path*',
// Also include login to handle redirect for already-authenticated users'/login',
]
}
// Without matcher, this middleware runs on:// - Every page load (/_next/static/*.css, *.js, *.png)// - API calls (/api/auth/*, /api/*)// - Static files (/favicon.ico, /robots.txt)// - Images, fonts, everything//// With matcher, it runs ONLY on the paths listed above.// 50-100x fewer executions per page view.
Auth middleware without matcher runs on every static file request. For an app with 80 assets per page, the auth check runs 80 times per page view. Always add matcher to restrict to protected paths. The performance difference is 50-100x fewer middleware executions.
Production Insight
A team deployed auth middleware without matcher on a 50-asset-page app. Edge function invocations jumped from 500K/month to 25M/month. Bill went from $50 to $500.
Rule: auth middleware matcher should list PROTECTED paths, not 'everything except'. The explicit list is shorter, clearer, and faster.
Key Takeaway
Auth middleware must have an explicit matcher listing protected paths.
Without matcher, auth checks run on every static file — 80 checks per page view.
Custom middleware with auth() gives more control than the default export pattern.
getSession() vs auth() for Fresh DataTrade-offs between caching and real-time accuracygetSession()auth()Data FreshnessStale up to 2 hoursAlways freshCaching BehaviorAggressive default cacheNo cachingPerformance ImpactFast, cached responsesSlower, direct fetchUse CaseNon-critical UI componentsAuth-protected routesConfigurationNo extra setup neededRequires force-dynamicTHECODEFORGE.IO
thecodeforge.io
Nextjs Authentication Authjs Guide
JWT vs Database Sessions: Choosing Your Session Strategy
Auth.js supports two session strategies: JWT (default) and Database.
JWT sessions: The session data is encoded in a signed JWT cookie. No database lookup needed on every request. auth() decrypts the JWT using the AUTH_SECRET — an efficient operation that takes <1ms. Downside: you cannot revoke individual sessions without a blocklist. Increasing JWT size affects cookie size (typically stays under 4KB with basic user data).
Database sessions: The session ID is stored in a cookie. On every request, Auth.js queries the database to look up the full session. Supports immediate session revocation. Downside: every auth() call makes a database query — adds latency (~5-20ms) and database load.
The decision depends on your revocation requirements
JWT: For most apps. Lighter, faster, no database dependency for auth(). Revoke sessions by changing AUTH_SECRET (logs everyone out) or maintaining a blocklist.
Database: When you need to revoke individual sessions (admin banning a user, multi-device management). The database query cost is acceptable for most apps.
Hybrid approach: Use JWT for the main session (fast, no DB call) and a database session table only for tracking active sessions for revocation. auth() reads the JWT quickly; revocation checks query the DB only on sensitive operations (password change, admin actions).
JWT sessions are <1ms to read (cookie decryption). Database sessions require a DB query (5-20ms). Choose JWT for performance, database for per-session revocation. Hybrid: JWT with a lightweight revocation check on sensitive operations.
Production Insight
A team using database sessions was hitting 50ms per auth() call because the database was in a different region than the serverless function. Switched to JWT strategy — auth() dropped to <1ms. Session revocation was handled via a short JWT expiry (15 min) with refresh tokens.
Rule: if your database is geographically distant from your serverless runtime, use JWT strategy to avoid auth latency.
Key Takeaway
JWT strategy: faster (<1ms), no database dependency, cannot revoke individual sessions without blocklist.
Choose JWT for most apps. Use database only when you need active session revocation.
Provider Configuration: Credentials, OAuth, and Magic Links Compared
Auth.js supports multiple provider types:
Credentials provider: email/password authentication. You provide an authorize() function that validates credentials and returns a user object. All authentication logic is your responsibility — password hashing, rate limiting, brute force protection. Credentials provider does NOT support signIn with OAuth-like redirects — the session is created directly.
OAuth providers (Google, GitHub, Facebook, etc.): OAuth 2.0 / OpenID Connect flow. Auth.js handles the redirect, token exchange, and profile fetching. You configure the provider with your client ID and secret. No password management on your side. The tradeoff: users must have an account with the provider.
Magic link (Email provider): Send a one-time sign-in link to the user's email. Auth.js generates the token, sends the email (you provide a sendVerificationRequest function or use the built-in Nodemailer integration), and verifies the link on click. No password needed. Requires an email service.
For most production apps, use a combination: OAuth for social login speed, email/magic links for passwordless access, and credentials as a fallback. Auth.js supports multiple providers simultaneously.
The Credentials provider does NOT have built-in rate limiting or brute force protection. Implement your own: limit login attempts per IP/email, add CAPTCHA after N failures, and use bcrypt (cost factor 12+) for password hashing.
Production Insight
A team using only the Credentials provider was hit by a credential stuffing attack — 100K login attempts in 1 hour. No rate limiting was implemented. The database was locked by the query volume.
Rule: OAuth or magic links are more secure by default. If you need credentials, add rate limiting, CAPTCHA, and account lockout from day one.
Key Takeaway
OAuth providers handle authentication flow — use as primary auth method when possible.
Magic links provide passwordless auth with email validation — good UX but depends on email delivery reliability.
Session Callbacks: Adding Custom Data to JWT and Session
Auth.js callbacks let you augment the session object with custom data. The two main callbacks:
jwt callback: Runs whenever a JWT is created or updated (sign in, session access, token refresh). Add user data (id, role, permissions) to the token here.
session callback: Runs whenever the session is accessed (via auth() or getSession()). Maps data from the JWT to the session object exposed to your application.
Pattern: Add user's role and permissions to the JWT in the jwt callback. In the session callback, extract them into session.user. Your Server Components then access session.user.role for authorization checks.
Performance note: jwt callback runs on EVERY session access (every auth() call). Keep it lightweight — no database queries. If you need database data in the session, fetch it in the component, not in the callback.
JWT Callback = Data Source. Session Callback = Data Shape.
The jwt callback is where you FETCH data (from DB, API). The session callback is where you FORMAT data for consumption. Keep jwt callback lean — fetch only what changes rarely. Defer frequently-changing data to the component.
Production Insight
A team put a database query in the session callback to fetch the user's current credits. Every auth() call queried the DB — 500ms per call at scale. The fix: move credits to a separate component that queries independently, keep session callbacks for static role/permission data.
Rule: session callbacks run on every auth() call. Do NOT put expensive operations there. Cache frequently-changing data separately.
Key Takeaway
jwt callback runs on sign-in and token refresh — add stable user data (id, role) here.
session callback runs on every auth() call — maps JWT to session object.
Keep both callbacks lean — defer database queries and expensive operations to individual components.
Sign Out, Session Refresh, and Token Rotation Patterns
Auth.js handles sign out and session management with specific patterns:
Sign out: Call signOut() from a client component. This clears the session cookie and optionally redirects. For Server Actions, signOut() can be called from a 'use server' function.
Session refresh: Call update() from useSession hook (client) or redirect to force a new session read. For Server Components, every auth() call reads fresh from the JWT cookie — no explicit refresh needed after the cookie updates.
Token rotation: JWT tokens have a maxAge (default 30 days). The token is automatically refreshed on session access if updateAge has passed. For short-lived tokens (15 min), use a refresh token pattern: store a long-lived refresh token in the JWT and use it to fetch a short-lived access token from your auth server.
The signOut pattern matters: if you use database sessions, signOut deletes the session from the database. If you use JWT, signOut clears the cookie — but the JWT remains valid until expiry. For immediate JWT invalidation, maintain a blocklist in Redis or database.
JWT sign out only clears the cookie — the JWT remains valid until expiry. For immediate invalidation, maintain a blocklist (Redis/DB) checked in the jwt callback. Or use short JWT expiry (15 min) with long refresh tokens.
Production Insight
A team using JWT sessions thought signOut() invalidated the token. A user reported being able to access the dashboard for 30 minutes after sign out by re-using the old cookie. The JWT was still valid until its 30-day expiry.
Rule: JWT sign out only clears the client-side cookie. For true invalidation, use a blocklist, short expiry, or database sessions.
Key Takeaway
signOut() clears the cookie — but JWT remains valid until expiry.
For immediate session invalidation, use database sessions or a JWT blocklist.
Session refresh: use update() on client, auth() is always fresh on server.
● Production incidentPOST-MORTEMseverity: high
getSession() Cached by Data Cache — Users Saw Each Other's Data for 2 Hours
Symptom
Users reported seeing other people's names, order histories, and account settings after logging in. The first user to access the site after deployment saw their own data. Every subsequent user saw the same data. The issue persisted for approximately 2 hours (the default Data Cache TTL) before self-resolving. Support received 200+ tickets within the first hour. The team initially suspected a database breach or session injection attack.
Assumption
The team assumed getSession() always returned fresh data because it made an API call. They did not know that GET /api/auth/session is a route handler call, and route handler GET responses are cached by the Data Cache by default in Next.js 15+.
Root cause
The Server Component called getSession() which internally calls fetch('/api/auth/session'). This is a GET request to a Next.js route handler. The Data Cache, introduced in Next.js 15 and default in 16, cached the response. Since the response depends on the requesting user's session cookie, the Data Cache should not have cached it — but the Data Cache does not inspect the requesting user's identity. It caches based on URL, method, and headers configuration. The auth session endpoint did not set Cache-Control: no-cache or use force-dynamic, so the Data Cache treated it as a cacheable GET route. The first request's response (user A's session) was cached and returned for all subsequent requests (users B, C, D, etc.) for 2 hours.
Fix
Applied two fixes: (1) Added 'export const dynamic = "force-dynamic"' to /api/auth/[...nextauth]/route.ts to prevent the Data Cache from caching auth responses. (2) Replaced all getSession() calls in Server Components with auth() (the getUser-equivalent) which reads the session directly from the JWT cookie without making an HTTP request. This eliminated the data cache vulnerability entirely for server-side code.
Key lesson
getSession() makes an HTTP GET request — the Data Cache caches it. NEVER use getSession() in Server Components
auth() / getUser() reads the session directly — no HTTP call, no cache, always fresh
GET route handlers are cached by default — API routes handling auth MUST be force-dynamic
2 hours of cross-user data exposure is a P0 security incident — not a performance optimization
Test auth with two simultaneous users in staging before deploying to production
Production debug guideDiagnose stale sessions, caching issues, and middleware problems5 entries
Symptom · 01
Users see other users' data after login
→
Fix
Check if getSession() is used in Server Components. Replace with auth(). Also check if /api/auth/[...nextauth]/route.ts has dynamic = 'force-dynamic'. The Data Cache is caching auth responses.
Symptom · 02
Session data never updates after login/logout
→
Fix
Check if the auth route handler is cached. Add export const dynamic = 'force-dynamic' to the [...nextauth] route. Clear the Data Cache with next cache: { revalidate: 0 } or revalidatePath('/api/auth/session').
Symptom · 03
Middleware auth check runs on all pages including static files
→
Fix
Add matcher config to middleware.ts. Without matcher, auth middleware checks every request including CSS, JS, images. Use matcher to restrict to protected routes only.
Symptom · 04
Session shows as authenticated but middleware redirects to login
→
Fix
Check if middleware and Server Component use the same auth method. Middleware using getSession() (cached) and Server Components using auth() (direct) can see different session states. Use auth() consistently.
Symptom · 05
OAuth callback fails with 'invalid state' error
→
Fix
Check that the AUTH_SECRET environment variable is set and consistent across deployments. State parameter is encrypted with this secret. Also check that the callback URL matches the provider configuration exactly.
★ Auth.js Debug Quick ReferenceFast commands and fixes for common Auth.js issues
Sign Out, Session Refresh, and Token Rotation Patterns
Key takeaways
1
NEVER use getSession() in Server Components
the Data Cache caches it. Use auth() instead
2
Add 'export const dynamic = "force-dynamic"' to the auth route handler
prevents Data Cache from caching session responses
3
Auth middleware must have an explicit matcher
without it, auth checks run on every static file request
4
Use auth() consistently in middleware, Server Components, Server Actions, and Route Handlers
5
JWT strategy is faster (<1ms) than database strategy (5-20ms)
choose based on revocation requirements
6
JWT sign out clears the cookie but does not invalidate the token
use short expiry or blocklist for immediate revocation
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the difference between getSession() and auth() in Auth.js v5 and...
Q02SENIOR
How do you configure Auth.js middleware with proper route protection?
Q03SENIOR
What is the JWT vs database session tradeoff in Auth.js?
Q04SENIOR
How do you add role-based access control (RBAC) to Auth.js sessions?
Q01 of 04SENIOR
Explain the difference between getSession() and auth() in Auth.js v5 and why using the wrong one caused a production incident.
ANSWER
getSession() makes an HTTP GET request to /api/auth/session — the Next.js route handler. This is a network call that goes through the Data Cache system. In Next.js 15+, GET route handlers are cached by default via the Data Cache. The auth route handler returns session data that depends on the requesting user's cookie — but the Data Cache caches by URL, not by user. The first user's response is cached and returned to all subsequent users.
auth() (equivalent to getUser in some Auth.js versions) reads the session directly from the session source — it decrypts the JWT cookie using the AUTH_SECRET (JWT strategy) or queries the session table in the database (database strategy). No HTTP call, no route handler invocation, no Data Cache involvement. The response is always per-user and always fresh.
The production incident: an e-commerce platform used getSession() in Server Components to fetch the current user. The Data Cache cached the first user's session response at /api/auth/session. For 2 hours (the default cache duration), every subsequent GET request returned user A's session data — all users saw user A's profile, orders, and account information. 200+ support tickets were filed before the team identified the cache as the root cause.
The fix: (1) replace getSession() with auth() in all Server Components, (2) add 'export const dynamic = "force-dynamic"' to the auth route handler to prevent future caching. The lesson: never use getSession() in Server Components — the Data Cache makes it unsafe for user-specific data.
Q02 of 04SENIOR
How do you configure Auth.js middleware with proper route protection?
ANSWER
Auth.js middleware is configured in middleware.ts at the root of the project. The recommended pattern is:
1. Import the auth function from your Auth.js configuration
2. Export a default middleware function that wraps auth()
3. Inside the middleware, check if the session exists for protected paths
4. If no session, redirect to login with a callbackUrl
5. If authenticated but accessing login, redirect to dashboard
6. CRITICAL: Add a matcher config that restricts middleware execution to protected paths only
The matcher is not optional. Without it, the auth middleware runs on every request — CSS files, JS chunks, images, fonts, API routes. For an app with 80 assets per page, that is 80 auth checks per page view. The matcher should explicitly list protected paths:
export const config = {
matcher: ['/dashboard/:path', '/account/:path', '/admin/:path*', '/login']
}
This limits auth checks to only the paths that need protection. Edge function invocations drop from 25M/month to 500K/month in a typical app.
Q03 of 04SENIOR
What is the JWT vs database session tradeoff in Auth.js?
ANSWER
The JWT strategy encodes session data in a signed cookie. auth() decrypts this cookie using the AUTH_SECRET — an operation that takes under 1ms and requires no external service. The tradeoff: you cannot revoke individual sessions. The JWT remains valid until its maxAge expiry (configurable, default 30 days). Sign out only clears the cookie — the JWT can be reused. For applications that need per-session revocation (admin banning a user, multi-device management), you need a blocklist checked on every auth() call, which adds complexity.
The database strategy stores session IDs in a cookie and session data in a database table. auth() reads the cookie, queries the database for the session, and returns the data. This adds 5-20ms per auth() call depending on database latency. The benefit: deleting a row from the session table immediately revokes that session. No JWT reuse possible.
For most applications, JWT is the better choice — it is faster, cheaper (no database query per request), and simpler to deploy. Use database sessions only when you have a specific requirement for per-session revocation, such as admin panels with user management features or compliance requirements for session control.
Q04 of 04SENIOR
How do you add role-based access control (RBAC) to Auth.js sessions?
ANSWER
RBAC is implemented through Auth.js callbacks that add role and permission data to the session:
1. In the jwt callback, during sign-in, fetch the user's role and permissions from the database and attach them to the token. This data is encoded in the JWT and persists across requests.
2. In the session callback, map the JWT data to the session object. This runs on every auth() call and makes the data available to your components.
3. In your Server Components and Server Actions, read session.user.role and session.user.permissions for authorization checks.
4. For client-side checks, expose role/permission data through the useSession hook and conditionally render UI elements.
The key insight: authorization data should be in the JWT (not fetched on every request) to keep auth() fast. Role changes require a token refresh or forced re-login. For frequently-changing permissions, fetch them independently in the component rather than embedding them in the session.
01
Explain the difference between getSession() and auth() in Auth.js v5 and why using the wrong one caused a production incident.
SENIOR
02
How do you configure Auth.js middleware with proper route protection?
SENIOR
03
What is the JWT vs database session tradeoff in Auth.js?
SENIOR
04
How do you add role-based access control (RBAC) to Auth.js sessions?
SENIOR
FAQ · 7 QUESTIONS
Frequently Asked Questions
01
What is the difference between getSession() and auth() in Auth.js v5?
getSession() makes an HTTP GET request to /api/auth/session which is subject to the Next.js Data Cache. auth() reads the session directly from the JWT cookie or database — no HTTP call, no cache. Use auth() in Server Components and Route Handlers. Use getSession() only in client components via the useSession hook.
Was this helpful?
02
How do I protect routes with Auth.js middleware?
Export the auth middleware from middleware.ts and add a matcher config that lists protected paths. Use export default auth(function middleware(request) { ... }) for custom logic. Always add export const config = { matcher: ['/dashboard/:path', '/account/:path'] } to restrict auth checks to specific routes. Without matcher, the middleware runs on every static file request.
Was this helpful?
03
What happens if I forget to add force-dynamic to the auth route handler?
The Data Cache caches GET /api/auth/session responses. The first user's session is cached and returned to ALL users for the cache duration. This is a P0 security incident — users see other users' data. Always add export const dynamic = 'force-dynamic' to app/api/auth/[...nextauth]/route.ts. Add this to your CI pipeline as a mandatory check.
Was this helpful?
04
Should I use JWT or database sessions?
JWT sessions are faster (<1ms per auth() call) and do not require a database query on every request. They are suitable for most applications. However, individual JWT sessions cannot be revoked without a blocklist. Database sessions support per-session revocation (admin can delete a specific session) but require a database query on every auth() call (5-20ms). Choose JWT for performance, database for per-session revocation requirements.
Was this helpful?
05
How do I add custom data (like user role) to the session?
Use the jwt and session callbacks in your Auth.js configuration. In the jwt callback (runs on sign-in and token refresh), add custom data like user role, permissions, or plan tier to the token. In the session callback (runs on every auth() call), map the JWT data to the session object. Your components then access session.user.role, session.user.permissions, etc. Keep both callbacks lightweight — defer expensive operations to individual components.
Was this helpful?
06
Does sign out invalidate the JWT token?
No — signOut() clears the session cookie from the browser, but the JWT token remains valid until its maxAge expiry (default 30 days). If a user re-applies the old cookie value, they can still access the session until the JWT expires. For immediate invalidation, use short JWT expiry (15 minutes) with refresh tokens, maintain a blocklist (checked in the jwt callback), or use database sessions.
Was this helpful?
07
Can I use Auth.js with Credentials provider in Edge Runtime?
The Credentials provider itself works in Edge Runtime, but password comparison libraries like bcrypt use Node.js APIs that are not available on Edge. If you use Credentials provider, you must use Node.js runtime for the auth route handler, or implement password comparison using Web Crypto APIs (which is complex). OAuth providers (GitHub, Google) work on Edge Runtime without issues.