Auth.js v5 (next-auth@beta) is the current production standard for Next.js authentication in 2026, offering native App Router support, Edge runtime compatibility, and a dramatically simplified API that replaces the v4 getServerSession() pattern with a single auth() function. For AI applications specifically, Auth.js v5 provides the foundation layer upon which token-aware rate limiting, MCP server authorization, and agent delegation chains can be built.

Why Authentication for Next.js AI Apps Is Different in 2026

Authentication for Next.js AI applications in 2026 fundamentally differs from traditional web apps because AI systems introduce three new attack surfaces and cost vectors that standard session management was never designed to handle. First, stateful context management: AI chat applications maintain multi-turn conversation state that must be tied to authenticated sessions — without this, attackers can hijack context windows. Second, token-aware rate limiting: a single unauthorized GPT-4 API call consuming 2,000 tokens costs roughly 100x more than a simple database read, meaning unauthorized access can cost thousands of dollars per hour (AIMultiple Research, 2025). Third, agent delegation chains: modern AI systems spawn child agents that must inherit authentication scope without re-prompting users. The average cost per AI-specific breach reached $4.80 million in 2025 (IBM Report), and 90% of organizations implementing AI report feeling unprepared for security risks. Traditional auth libraries like NextAuth v4 were designed for human-to-server interactions; Auth.js v5 bridges the gap by providing Web Standard APIs, Edge runtime compatibility, and enough extensibility to build the additional AI-specific layers on top.

The Evolution of NextAuth.js: From Pages Router Library to Auth.js Framework

NextAuth.js v5 represents a complete architectural rewrite of the library, now renamed Auth.js to reflect its framework-agnostic design that extends to SvelteKit, SolidStart, Express, and Qwik — not just Next.js. The original NextAuth was built Pages Router-first, with configuration living in pages/api/auth/[...nextauth].ts and session retrieval requiring getServerSession() with explicit imports from next-auth/next. This worked well for 2021-era Next.js apps but broke down under App Router’s server-component model where every component can independently access session data.

Auth.js v5 (next-auth@beta) currently has 22K+ GitHub stars and 111K+ weekly npm downloads, making it the de facto standard for Next.js authentication. The rewrite adopts Web Standard APIs (Request/Response instead of Node.js-specific objects), which is what enables Edge runtime compatibility — critical for middleware that runs on Vercel’s edge network before your server-side code executes. The v5 design philosophy is: one auth() function that works everywhere, zero arguments needed in most cases, auto-detection of environment from request context.

The key architectural changes from v4:

  • Configuration moves from pages/api/auth/[...nextauth].ts → root-level auth.ts
  • getServerSession() replaced by auth() everywhere
  • Environment variables: NEXTAUTH_SECRETAUTH_SECRET, NEXTAUTH_URL → auto-detected
  • Adapter packages: @next-auth/*@auth/* namespace (e.g., @auth/prisma-adapter)
  • OAuth 1.0 support dropped entirely in v5

NextAuth.js v5 Core Architecture: The auth() Function

The auth() function is the cornerstone of Auth.js v5, replacing getServerSession() across all execution contexts — Server Components, Route Handlers, API Routes, middleware, and Server Actions. Auth.js v5’s architecture centers on a single exported auth function that auto-detects its execution context and returns the current session without requiring arguments in server-side contexts. In middleware, it wraps your handler function. In Server Components, it returns a Session | null directly. In Route Handlers, it also returns Session | null after parsing the incoming request. This unified API reduces cognitive overhead dramatically: you no longer need to remember whether you’re calling getServerSession(authOptions) or getToken({ req }) depending on context.

// auth.ts (root level) — the single source of truth
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub, Google],
  secret: process.env.AUTH_SECRET,
})
// app/api/auth/[...nextauth]/route.ts — just 2 lines
import { handlers } from "@/auth"
export const { GET, POST } = handlers
// Server Component usage
import { auth } from "@/auth"

export default async function Dashboard() {
  const session = await auth()
  if (!session?.user) redirect('/login')
  return <div>Welcome {session.user.name}</div>
}

Environment variable auto-detection means AUTH_URL is inferred from request headers in most deployment environments, so you typically only need AUTH_SECRET and your provider secrets (e.g., AUTH_GITHUB_ID, AUTH_GITHUB_SECRET).

Quick Start: Setting Up Auth.js v5 with Next.js App Router

Setting up Auth.js v5 in a new Next.js 15/16 App Router project requires installing three packages (next-auth@beta, your adapter if using database sessions, and bcryptjs for credentials), creating auth.ts at the project root, and wiring up the route handler. The complete setup takes under an hour for OAuth-only apps. For Credentials provider with database sessions, add two to four hours for schema setup and testing.

Step 1: Install dependencies

npm install next-auth@beta
# For database sessions (optional):
npm install @auth/prisma-adapter @prisma/client
# For Credentials provider (optional):
npm install bcryptjs
npm install -D @types/bcryptjs

Step 2: Create auth.ts at project root

import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: "jwt" }, // or "database"
  providers: [
    GitHub,
    Google,
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        const parsed = credentialsSchema.safeParse(credentials)
        if (!parsed.success) return null
        const user = await prisma.user.findUnique({
          where: { email: parsed.data.email }
        })
        if (!user?.password) return null
        const valid = await bcrypt.compare(parsed.data.password, user.password)
        return valid ? user : null
      }
    })
  ],
  callbacks: {
    jwt({ token, user }) {
      if (user) token.role = user.role // persist role on sign-in
      return token
    },
    session({ session, token }) {
      session.user.role = token.role as string
      return session
    }
  }
})

Step 3: Create route handler

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers

Step 4: Add environment variables

AUTH_SECRET="your-secret-here" # openssl rand -base64 32
AUTH_GITHUB_ID="your-github-app-id"
AUTH_GITHUB_SECRET="your-github-app-secret"
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"

JWT vs Database Sessions: Choosing the Right Strategy for AI Applications

Auth.js v5 supports two session strategies, and the choice between them has significant implications for AI application security and performance. JWT sessions store encrypted session data in a browser cookie — each request is self-contained, no database round-trip required, making them faster but impossible to invalidate until cookie expiry. Database sessions store a reference token in the cookie and look up session data on every authenticated request — one extra DB query per request but immediate revocability. For AI applications, this tradeoff becomes critical because compromised AI agent sessions can incur massive costs before the expiry window closes.

StrategyRequest LatencySession RevocationBest For
JWT~0ms extraOnly at expiryConsumer AI apps, stateless APIs
Database+5–15ms DB queryImmediateB2B/enterprise, agent delegation chains

When to use JWT sessions:

  • Consumer-facing AI chat apps where instant revocation is not required
  • Edge-deployed API routes where no database connection is available
  • High-traffic AI endpoints where per-request DB queries would bottleneck

When to use Database sessions:

  • B2B/enterprise AI applications where admins must be able to force-logout compromised accounts
  • AI agent systems where parent agents spawn child agents (delegation chains require traceable session lineage)
  • Any AI app processing sensitive enterprise data where compliance mandates immediate revocation capability

For most AI applications in 2026, the recommended approach is JWT for the user-facing web app layer, combined with short-lived API keys or database-backed tokens for AI agent-to-service communication.

Protecting Routes: middleware.ts, Server Components, API Routes, and Server Actions

Auth.js v5 provides four distinct protection patterns corresponding to Next.js’s four execution contexts. Route protection in Auth.js v5 works through the auth() function in all four execution contexts — middleware wraps the handler function to intercept unauthenticated requests before they reach your code, Server Components call auth() directly and redirect, Route Handlers call auth() at the top of the handler, and Server Actions call auth() before performing mutations. The middleware pattern is the most efficient for protecting large groups of routes because it runs at the CDN edge before any server code executes.

Middleware protection (most efficient for grouped routes):

// middleware.ts — auth.config.ts split required for Edge compatibility
import { auth } from "./auth"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isApiRoute = req.nextUrl.pathname.startsWith('/api')
  const isAuthPage = req.nextUrl.pathname.startsWith('/auth')
  
  if (!isLoggedIn && !isAuthPage) {
    return Response.redirect(new URL('/auth/login', req.nextUrl))
  }
  
  if (isApiRoute && !isLoggedIn) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), { 
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    })
  }
})

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

The matcher pattern is critical — failing to exclude _next/static, _next/image, and static assets causes the middleware to run on every static file request, significantly degrading performance.

Server Component protection:

import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function AIChat() {
  const session = await auth()
  if (!session?.user) redirect('/auth/login')
  // session.user.role available if RBAC is configured
  return <ChatInterface userId={session.user.id} />
}

API Route protection:

import { auth } from "@/auth"
import { NextResponse } from "next/server"

export async function POST(req: Request) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  // AI API call logic here
}

Server Action protection:

"use server"
import { auth } from "@/auth"

export async function generateAIResponse(prompt: string) {
  const session = await auth()
  if (!session?.user) throw new Error('Unauthorized')
  // Server-side AI API call
}

Role-Based Access Control (RBAC) with Auth.js

Auth.js v5 implements RBAC through JWT callbacks that persist custom fields to the session token, combined with TypeScript module augmentation that extends the default Session and User types. RBAC with Auth.js v5 uses the jwt() callback to attach role data to the encrypted token on sign-in, the session() callback to expose that role on the client-accessible session object, and TypeScript module augmentation in next-auth.d.ts to add type safety throughout the app. The Prisma schema requires a role field on the User model, which Auth.js reads via the adapter on sign-in and persists to the JWT.

Prisma schema for RBAC:

enum Role {
  USER
  ADMIN
  AI_OPERATOR
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  password      String?
  role          Role      @default(USER)
  accounts      Account[]
  sessions      Session[]
}

TypeScript module augmentation:

// next-auth.d.ts
import "next-auth"
import { Role } from "@prisma/client"

declare module "next-auth" {
  interface User {
    role?: Role
  }
  interface Session {
    user: {
      role?: Role
    } & DefaultSession["user"]
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    role?: Role
  }
}

RBAC middleware for AI admin routes:

export default auth((req) => {
  const session = req.auth
  const isAdminRoute = req.nextUrl.pathname.startsWith('/admin')
  const isAIOperatorRoute = req.nextUrl.pathname.startsWith('/api/ai')
  
  if (isAdminRoute && session?.user?.role !== 'ADMIN') {
    return Response.redirect(new URL('/unauthorized', req.nextUrl))
  }
  
  if (isAIOperatorRoute && !['ADMIN', 'AI_OPERATOR'].includes(session?.user?.role ?? '')) {
    return new Response(JSON.stringify({ error: 'Insufficient permissions' }), { status: 403 })
  }
})

AI-Specific Authentication Challenges

AI applications require authentication patterns that traditional web frameworks never anticipated, including stateful context management across multi-turn conversations, session persistence through streaming responses, and agent delegation chains where parent AI agents spawn child agents that must operate within a bounded authorization scope. Stateful context management means tying conversation history to an authenticated session identifier so that prompt injection attacks cannot hijack another user’s context window. Streaming response session persistence is a technical challenge unique to AI: HTTP streaming keeps connections open for 30–120 seconds, during which the session must remain valid and checked continuously. Agent delegation chains require that when an AI orchestrator agent spawns a tool-calling sub-agent, that sub-agent inherits a scoped, non-escalatable token — not the full user session.

Stateful AI context session management:

// lib/ai-session.ts
import { auth } from "@/auth"

export async function getAISessionContext() {
  const session = await auth()
  if (!session?.user?.id) throw new Error('Unauthenticated')
  
  // Fetch conversation context tied to this session
  const context = await prisma.aiConversation.findFirst({
    where: { userId: session.user.id, active: true },
    include: { messages: { orderBy: { createdAt: 'asc' }, take: 20 } }
  })
  
  return { session, context }
}

Scoped agent tokens for AI delegation chains:

// Generate a time-limited, scoped token for child agent operations
export async function createAgentDelegationToken(
  parentSession: Session,
  scope: string[],
  ttlSeconds: number = 300
) {
  const token = await encrypt({
    sub: parentSession.user.id,
    parentSessionId: parentSession.id,
    scope,
    exp: Math.floor(Date.now() / 1000) + ttlSeconds,
  })
  return token
}

Security in the AI Era: CVE-2025-29927 and OWASP Top 10 for LLMs

CVE-2025-29927 (CVSS 9.1) is one of the most critical authentication vulnerabilities in Next.js history — a complete middleware authentication bypass achieved by injecting a x-middleware-subrequest header into HTTP requests, affecting versions 15.x below 15.2.3 and 14.x below 14.2.25. The vulnerability allows an attacker to skip all middleware execution, including auth checks, by setting this single header — no credentials required. If your Next.js middleware is the only authentication layer (a common pattern), this means complete authentication bypass for any unauthenticated request. The fix: update to Next.js 15.2.3+ or 14.2.25+, and never rely solely on middleware for authentication — always validate sessions in Server Components and API Routes as well.

Defense-in-depth authentication checklist for AI apps:

LayerVulnerabilityMitigation
MiddlewareCVE-2025-29927 header injectionUpdate Next.js, add redundant auth checks
Server ComponentsMissing auth() callAlways call auth() before rendering sensitive content
API RoutesSkipped auth checkCall auth() at top of every handler
AI API callsPrompt injectionSanitize user input, system prompt isolation
Streaming responsesSession expiry during streamRe-validate session before each chunk

OWASP Top 10 for LLMs — authentication-relevant items:

  • LLM01: Prompt Injection — success rates exceed 50% on unprotected models; multi-language attacks succeed at 70%+ (Lakera Research, 2025). Mitigation: authenticated session context should be injected via system prompt before user input, not concatenated with it.
  • LLM06: Sensitive Information Disclosure — AI models can leak data from other users’ contexts if session isolation is not enforced at the database query layer.
  • LLM09: Overreliance — Systems that trust AI outputs without verifying the authenticated user authorized the resulting actions.
// Secure AI API route with defense-in-depth
export async function POST(req: Request) {
  const session = await auth() // Layer 2 auth check (middleware is Layer 1)
  if (!session?.user) return new Response('Unauthorized', { status: 401 })
  
  const { prompt } = await req.json()
  const sanitizedPrompt = sanitizePromptInput(prompt) // Remove injection attempts
  
  const systemPrompt = `You are assisting ${session.user.name} (ID: ${session.user.id}).
Only access data belonging to this user. Never reveal data from other users.
User's permissions: ${session.user.role}`
  
  // AI call with isolated context
}

Token-Aware Rate Limiting: Protecting Against Runaway AI API Costs

Token-aware rate limiting is a security control unique to AI applications — unlike traditional rate limiting that counts requests per minute (RPM), token-aware limiting tracks the cumulative token cost of AI API calls per authenticated user session, because a single 2,000-token GPT-4 Turbo request costs roughly 100x more than a 20-token prompt, making RPM limits alone economically inadequate. Unauthorized AI API usage can cost thousands of dollars per hour (AIMultiple Research, 2025). Implementing token-aware rate limiting requires storing token usage per user in Redis or your database, checking remaining budget before each AI call, and returning a 429 with a Retry-After header when the budget is exhausted.

// lib/rate-limit.ts — token-aware rate limiting for AI endpoints
import { auth } from "@/auth"

interface RateLimitConfig {
  maxTokensPerHour: number
  maxRequestsPerMinute: number
}

const DEFAULT_LIMITS: Record<string, RateLimitConfig> = {
  USER: { maxTokensPerHour: 50_000, maxRequestsPerMinute: 10 },
  AI_OPERATOR: { maxTokensPerHour: 500_000, maxRequestsPerMinute: 60 },
  ADMIN: { maxTokensPerHour: 2_000_000, maxRequestsPerMinute: 200 },
}

export async function checkAIRateLimit(userId: string, role: string) {
  const limits = DEFAULT_LIMITS[role] ?? DEFAULT_LIMITS.USER
  
  const [hourlyUsage, minuteRequests] = await Promise.all([
    redis.get(`ai:tokens:${userId}:${hourKey()}`),
    redis.get(`ai:requests:${userId}:${minuteKey()}`),
  ])
  
  if (parseInt(hourlyUsage ?? '0') >= limits.maxTokensPerHour) {
    throw new Error(`Hourly token limit exceeded (${limits.maxTokensPerHour} tokens/hour)`)
  }
  
  if (parseInt(minuteRequests ?? '0') >= limits.maxRequestsPerMinute) {
    throw new Error(`Rate limit exceeded (${limits.maxRequestsPerMinute} req/min)`)
  }
}

export async function trackAITokenUsage(userId: string, tokensUsed: number) {
  const pipeline = redis.pipeline()
  pipeline.incrby(`ai:tokens:${userId}:${hourKey()}`, tokensUsed)
  pipeline.expire(`ai:tokens:${userId}:${hourKey()}`, 3600)
  pipeline.incr(`ai:requests:${userId}:${minuteKey()}`)
  pipeline.expire(`ai:requests:${userId}:${minuteKey()}`, 60)
  await pipeline.exec()
}

Model Context Protocol (MCP) Authentication: A New Paradigm for AI Agent Authorization

Model Context Protocol (MCP) represents a new authentication paradigm for AI agent communication with external tools — standardizing how AI assistants like Claude authenticate to external services (databases, APIs, file systems) without exposing full user credentials. MCP authentication sits above Auth.js user authentication: Auth.js proves who the human user is, while MCP authentication controls what tools an AI agent can access on behalf of that user. An authenticated user session does not automatically grant AI agents access to MCP servers — that authorization must be explicitly granted and scoped.

MCP authorization flow with Auth.js:

UUAAMssuICeetPrrha.gsaajeeuusnrtttvhhseeoeprnrsrtisevizisaceoelasnnittdeAsasItsdevaiesisglasnetiegoAsdakuttetadinhneo,.tlnjegstgtroaoatknaietUconsscneetsrstocsooskMpeseCespnPdseicwstoiieonftroihvlecestrattocaoocbolellsisssschoepdes
// lib/mcp-auth.ts — MCP server authorization using Auth.js session
import { auth } from "@/auth"

export async function createMCPDelegationToken(
  toolScopes: string[],  // e.g., ['read:database', 'write:files']
  expiresInSeconds: number = 300
) {
  const session = await auth()
  if (!session?.user) throw new Error('Unauthenticated')
  
  return await signMCPToken({
    sub: session.user.id,
    scopes: toolScopes,
    iss: process.env.NEXTAUTH_URL ?? process.env.AUTH_URL!,
    aud: 'mcp-server',
    exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
  })
}

78% of enterprises have adopted AI, yet 71% regularly use generative AI in production (G2 Research, 2025), making MCP authentication a near-term production requirement rather than a future consideration.

v4 → v5 Migration: Breaking Changes and Testing Strategy

Migrating from NextAuth v4 to Auth.js v5 takes approximately half a day of mechanical edits plus one day of testing — two days if your app has custom database adapter logic (Web3 AI Blog). The migration is almost entirely mechanical: file moves, package renames, and environment variable renames. No database schema changes are required. OAuth 1.0 providers (Twitter API v1) need replacement. The two-file split for Edge compatibility is the only structural change that requires genuine architectural thinking.

Complete breaking changes checklist:

v4 Patternv5 Replacement
pages/api/auth/[...nextauth].tsauth.ts at root + app/api/auth/[...nextauth]/route.ts
getServerSession(authOptions)auth()
useSession() from next-auth/reactSame (unchanged)
NEXTAUTH_SECRETAUTH_SECRET
NEXTAUTH_URLAUTH_URL (auto-detected, often not needed)
@next-auth/prisma-adapter@auth/prisma-adapter
NextAuthOptions typeNextAuthConfig type
withAuth middleware wrapperauth() wrapper function
getToken({ req }) in API routesauth()

Migration testing strategy:

  1. Run both v4 and v5 auth configurations in parallel using feature flags during testing
  2. Test OAuth flows for each provider (Google requires explicit scope: 'openid email profile' in v5)
  3. Verify JWT token shape if you rely on custom JWT fields
  4. Test middleware matcher patterns — v5 middleware matcher syntax may need updates
  5. Verify Drizzle adapter users: v5 has the largest schema delta, check reference schema

Edge runtime two-file split (required if using adapters):

// auth.config.ts — edge-safe, NO adapter, NO Node.js-only imports
import type { NextAuthConfig } from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"

export const authConfig: NextAuthConfig = {
  providers: [GitHub, Google],
  pages: { signIn: '/auth/login' },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isProtected = nextUrl.pathname.startsWith('/dashboard')
      if (isProtected) return isLoggedIn
      return true
    }
  }
}
// auth.ts — full config with adapter (Node.js runtime only)
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import { authConfig } from "./auth.config"

export const { handlers, signIn, signOut, auth } = NextAuth({
  ...authConfig,
  adapter: PrismaAdapter(prisma),
  session: { strategy: "jwt" },
})
// middleware.ts — imports from auth.config.ts, NOT auth.ts
import NextAuth from "next-auth"
import { authConfig } from "./auth.config"

export default NextAuth(authConfig).auth

export const config = {
  matcher: ['/((?!api/auth|_next/static|_next/image|.*\\.png$).*)']
}

Provider Comparison: DIY vs Auth.js vs Clerk vs Authgear for AI Apps

Choosing the right authentication approach for a Next.js AI application involves three realistic options in 2026: DIY implementation (weeks to months, full control), Auth.js v5 (hours, free OSS, good for consumer apps), or managed platforms like Clerk or Authgear (minutes, production-ready features, monthly cost). DIY authentication’s hidden cost is that every new auth method — passkeys, SAML SSO, MFA, WebAuthn — is a fresh engineering project, and every CVE is yours to patch. Auth.js v5 is the sweet spot for bootstrapped AI apps and consumer products, covering OAuth, credentials, magic links, and basic RBAC out of the box. Managed platforms become necessary when B2B multi-tenant isolation, SOC 2 compliance, built-in fraud protection, or enterprise SSO are requirements.

FeatureDIYAuth.js v5ClerkAuthgear
Setup timeWeeksHoursMinutesMinutes
CostDev time onlyFree OSS$25–$100+/mo$49+/mo
OAuth providersManual80+ built-in30+ built-in20+ built-in
Passkeys/WebAuthnWeeks of workExperimental✅ Production✅ Production
SAML/SSOSignificant workCustom only
AI agent toolkitBuild yourselfBuild yourself✅ @clerk/agent-toolkitLimited
MCP authenticationBuild yourselfBuild yourselfVia agent toolkitLimited
Fraud protectionBuild yourselfNone
Edge runtimeManual✅ (two-file split)
CVE responsibilityYoursAuth.js teamProviderProvider

Auth.js v5 is explicitly the right choice for: early-stage AI apps, consumer products without SSO requirements, and teams where open-source extensibility outweighs managed convenience. Clerk’s @clerk/agent-toolkit provides automatic session context injection into AI system prompts, making it purpose-built for AI applications when budget allows.

Enterprise Requirements: Multi-Tenant Isolation and Compliance

Enterprise Next.js AI applications require authentication infrastructure that goes significantly beyond what Auth.js v5 provides out of the box — specifically multi-tenant data isolation, SAML/SSO integration, SOC 2 audit logging, and forced session revocation at the organization level. Auth.js v5 can serve as the foundation for multi-tenant apps, but tenant isolation must be implemented at the data access layer (Prisma query middleware or RLS in PostgreSQL), not at the authentication layer. Each authenticated session should carry a tenantId claim in the JWT, and every database query must be scoped to that tenant.

Multi-tenant JWT with Auth.js:

// auth.ts callbacks for multi-tenant
callbacks: {
  jwt({ token, user, account }) {
    if (user?.tenantId) {
      token.tenantId = user.tenantId
      token.role = user.role
    }
    return token
  },
  session({ session, token }) {
    session.user.tenantId = token.tenantId as string
    session.user.role = token.role as string
    return session
  }
}

Prisma middleware for tenant isolation:

prisma.$use(async (params, next) => {
  const session = await auth()
  if (!session?.user?.tenantId) throw new Error('Missing tenant context')
  
  if (params.action === 'findMany' || params.action === 'findFirst') {
    params.args.where = {
      ...params.args.where,
      tenantId: session.user.tenantId,
    }
  }
  return next(params)
})

Frequently Asked Questions

Q: What is the difference between NextAuth.js and Auth.js?

Auth.js is the new name for NextAuth.js v5. The rename reflects the library’s expansion beyond Next.js — Auth.js v5 supports SvelteKit, SolidStart, Express, Qwik, and Next.js. For Next.js, you still install next-auth@beta and import from next-auth. The two names refer to the same library at v5+; “NextAuth.js” typically refers to v4 and earlier.

Q: Is Auth.js v5 production-ready in 2026?

Yes. Auth.js v5 (next-auth@beta) is production-ready and widely deployed — it has 22K+ GitHub stars and 111K+ weekly npm downloads. The “beta” label reflects API stability guarantees, not production readiness. The Auth.js team has indicated a stable v5 release is pending. For new projects starting in 2026, v5 is the recommended choice.

Q: Should I use JWT or database sessions for an AI chatbot?

For consumer AI chatbots: JWT sessions are usually sufficient — they’re faster (no DB query per request) and the risk of an un-revocable compromised session is low for consumer apps. For B2B AI products: use database sessions so admins can force-logout compromised accounts immediately. The extra 5–15ms per request is worth the security guarantee when enterprise data is involved.

Q: How do I handle authentication with streaming AI responses?

Validate the session before initiating the stream, not during. Once the stream starts, you cannot easily interrupt it for auth re-validation without breaking the streaming connection. The pattern: const session = await auth() → validate → start ai.streamText() → pipe to Response. Set stream TTL shorter than session expiry (e.g., max 60-second streams if session expires in 30 minutes).

Q: How do I fix the CVE-2025-29927 middleware bypass vulnerability?

Update Next.js to 15.2.3+ (for Next.js 15) or 14.2.25+ (for Next.js 14). Verify with npm list next. Also add defense-in-depth by calling auth() inside Server Components and API Routes, not just relying on middleware. Never treat middleware as the sole authentication layer.