Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    jackspace

    auth-js

    jackspace/auth-js
    Security
    8

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Production-ready Auth.js v5 setup for Next.js and Cloudflare Workers.

    Use when: setting up authentication, implementing OAuth/credentials/magic links, configuring D1 or PostgreSQL adapters,...

    SKILL.md

    Auth.js v5 Authentication Stack

    Production-tested: Multiple Next.js and Cloudflare Workers projects Last Updated: 2025-10-26 Status: Production Ready ✅ Official Docs: https://authjs.dev


    ⚠️ BEFORE YOU START (READ THIS!)

    CRITICAL FOR AI AGENTS: If you're Claude Code helping a user set up Auth.js:

    1. Explicitly state you're using this skill at the start of the conversation
    2. Reference patterns from the skill rather than general knowledge
    3. Prevent known issues listed in references/common-errors.md
    4. Don't guess - if unsure, check the skill documentation

    USER ACTION REQUIRED: Tell Claude to check this skill first!

    Say: "I'm setting up Auth.js - check the auth-js skill first"

    Why This Matters (Real-World Results)

    Without skill activation:

    • ❌ Setup time: ~15 minutes
    • ❌ Errors encountered: 3-5 (AUTH_SECRET, CallbackRouteError, edge issues)
    • ❌ Manual fixes needed: 3-4 commits
    • ❌ Token usage: ~15k
    • ❌ User confidence: Multiple debugging sessions

    With skill activation:

    • ✅ Setup time: ~3 minutes
    • ✅ Errors encountered: 0
    • ✅ Manual fixes needed: 0
    • ✅ Token usage: ~6k (60% reduction)
    • ✅ User confidence: Instant success

    Known Issues This Skill Prevents

    1. Missing AUTH_SECRET → JWEDecryptionFailed error
    2. CallbackRouteError → Throwing in authorize() instead of returning null
    3. Route not found → Incorrect file path for [...nextauth].js
    4. Edge incompatibility → Using database session without edge-compatible adapter
    5. PKCE errors → OAuth provider misconfiguration
    6. Session not updating → Missing middleware
    7. v5 migration issues → Namespace changes, JWT salt changes
    8. D1 binding errors → Wrangler configuration mismatch
    9. Credentials with database → Incompatible session strategy
    10. Production deployment failures → Missing environment variables
    11. Token refresh errors → Incorrect callback implementation
    12. JSON expected but HTML received → Rewrites configuration in Next.js 15

    All of these are handled automatically when the skill is active.


    Table of Contents

    1. Quick Start - Next.js
    2. Quick Start - Cloudflare Workers
    3. Core Concepts
    4. Session Strategies
    5. Provider Setup
    6. Database Adapters
    7. Middleware Patterns
    8. Advanced Features
    9. Critical Rules
    10. Common Errors & Fixes
    11. Templates Reference

    Quick Start: Next.js

    Prerequisites

    # Next.js 15+ with App Router
    npm create next-app@latest my-app
    cd my-app
    

    Installation

    npm install next-auth@latest
    npm install @auth/core@latest
    
    # Choose your database adapter (if using database sessions)
    npm install @auth/prisma-adapter  # For PostgreSQL/MySQL
    npm install @auth/d1-adapter      # For Cloudflare D1
    

    1. Create Auth Configuration

    Option A: Simple Setup (JWT sessions, no database)

    // auth.ts
    import NextAuth from "next-auth"
    import GitHub from "next-auth/providers/github"
    
    export const { handlers, auth, signIn, signOut } = NextAuth({
      providers: [
        GitHub({
          clientId: process.env.AUTH_GITHUB_ID,
          clientSecret: process.env.AUTH_GITHUB_SECRET,
        }),
      ],
    })
    

    Option B: Edge-Compatible Setup (recommended for middleware)

    // auth.config.ts (edge-compatible, no database)
    import type { NextAuthConfig } from "next-auth"
    import GitHub from "next-auth/providers/github"
    
    export default {
      providers: [
        GitHub({
          clientId: process.env.AUTH_GITHUB_ID,
          clientSecret: process.env.AUTH_GITHUB_SECRET,
        }),
      ],
    } satisfies NextAuthConfig
    
    // auth.ts (full config with database)
    import NextAuth from "next-auth"
    import { PrismaAdapter } from "@auth/prisma-adapter"
    import { PrismaClient } from "@prisma/client"
    import authConfig from "./auth.config"
    
    const prisma = new PrismaClient()
    
    export const { handlers, auth, signIn, signOut } = NextAuth({
      adapter: PrismaAdapter(prisma),
      session: { strategy: "jwt" }, // CRITICAL: Force JWT for edge compatibility
      ...authConfig,
    })
    

    2. Create API Route Handler

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

    3. Add Middleware (Optional but Recommended)

    // middleware.ts
    export { auth as middleware } from "@/auth"
    
    export const config = {
      matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
    }
    

    4. Environment Variables

    # .env.local
    AUTH_SECRET=your-secret-here  # Generate with: npx auth secret
    AUTH_GITHUB_ID=your_github_client_id
    AUTH_GITHUB_SECRET=your_github_client_secret
    
    # CRITICAL: In production, AUTH_SECRET is REQUIRED
    

    5. Use in Components

    Server Component (App Router):

    import { auth } from "@/auth"
    
    export default async function Dashboard() {
      const session = await auth()
    
      if (!session?.user) {
        return <p>Not authenticated</p>
      }
    
      return <p>Welcome {session.user.name}</p>
    }
    

    Client Component:

    "use client"
    import { useSession } from "next-auth/react"
    
    export default function ClientDashboard() {
      const { data: session, status } = useSession()
    
      if (status === "loading") return <p>Loading...</p>
      if (status === "unauthenticated") return <p>Not authenticated</p>
    
      return <p>Welcome {session?.user?.name}</p>
    }
    

    6. Sign In / Sign Out

    import { signIn, signOut } from "@/auth"
    
    export function SignIn() {
      return (
        <form
          action={async () => {
            "use server"
            await signIn("github")
          }}
        >
          <button type="submit">Sign in with GitHub</button>
        </form>
      )
    }
    
    export function SignOut() {
      return (
        <form
          action={async () => {
            "use server"
            await signOut()
          }}
        >
          <button type="submit">Sign Out</button>
        </form>
      )
    }
    

    Quick Start: Cloudflare Workers

    Prerequisites

    npm create cloudflare@latest my-auth-worker
    cd my-auth-worker
    

    Installation

    npm install @auth/core@latest
    npm install @auth/d1-adapter@latest
    npm install hono
    

    1. Configure Wrangler with D1

    // wrangler.jsonc
    {
      "name": "my-auth-worker",
      "main": "src/index.ts",
      "compatibility_date": "2025-10-26",
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "auth_db",
          "database_id": "your-database-id"
        }
      ]
    }
    

    2. Create D1 Database

    # Create database
    npx wrangler d1 create auth_db
    
    # Copy the database_id to wrangler.jsonc
    
    # Create tables
    npx wrangler d1 execute auth_db --file=./schema.sql
    

    schema.sql:

    -- See templates/cloudflare-workers/schema.sql for complete schema
    CREATE TABLE users (
      id TEXT PRIMARY KEY,
      name TEXT,
      email TEXT UNIQUE NOT NULL,
      emailVerified INTEGER,
      image TEXT
    );
    
    CREATE TABLE accounts (
      id TEXT PRIMARY KEY,
      userId TEXT NOT NULL,
      type TEXT NOT NULL,
      provider TEXT NOT NULL,
      providerAccountId TEXT NOT NULL,
      refresh_token TEXT,
      access_token TEXT,
      expires_at INTEGER,
      token_type TEXT,
      scope TEXT,
      id_token TEXT,
      session_state TEXT,
      FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,
      UNIQUE(provider, providerAccountId)
    );
    
    CREATE TABLE sessions (
      id TEXT PRIMARY KEY,
      userId TEXT NOT NULL,
      expires INTEGER NOT NULL,
      sessionToken TEXT UNIQUE NOT NULL,
      FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
    );
    
    CREATE TABLE verification_tokens (
      identifier TEXT NOT NULL,
      token TEXT UNIQUE NOT NULL,
      expires INTEGER NOT NULL,
      PRIMARY KEY (identifier, token)
    );
    

    3. Create Worker with Auth

    // src/index.ts
    import { Hono } from 'hono'
    import { Auth } from '@auth/core'
    import { D1Adapter } from '@auth/d1-adapter'
    import GitHub from '@auth/core/providers/github'
    
    type Bindings = {
      DB: D1Database
      AUTH_SECRET: string
      AUTH_GITHUB_ID: string
      AUTH_GITHUB_SECRET: string
    }
    
    const app = new Hono<{ Bindings: Bindings }>()
    
    app.all('/api/auth/*', async (c) => {
      const response = await Auth(c.req.raw, {
        adapter: D1Adapter(c.env.DB),
        providers: [
          GitHub({
            clientId: c.env.AUTH_GITHUB_ID,
            clientSecret: c.env.AUTH_GITHUB_SECRET,
          }),
        ],
        secret: c.env.AUTH_SECRET,
        trustHost: true,
      })
      return response
    })
    
    app.get('/', async (c) => {
      // Example: Get session
      const session = await Auth(c.req.raw, {
        adapter: D1Adapter(c.env.DB),
        providers: [],
        secret: c.env.AUTH_SECRET,
      })
    
      return c.json({ session })
    })
    
    export default app
    

    4. Environment Variables

    # Add secrets to Cloudflare Workers
    npx wrangler secret put AUTH_SECRET
    npx wrangler secret put AUTH_GITHUB_ID
    npx wrangler secret put AUTH_GITHUB_SECRET
    

    5. Deploy

    npx wrangler deploy
    

    Core Concepts

    Session Strategies

    Auth.js v5 supports two session strategies:

    1. JWT (JSON Web Token) - Default when no adapter configured

      • ✅ Works in edge runtime
      • ✅ No database queries for sessions
      • ✅ Fast and scalable
      • ❌ Cannot invalidate sessions server-side
      • ❌ Token size limits
    2. Database - Default when adapter configured

      • ✅ Full session control (invalidate, extend)
      • ✅ Audit trail in database
      • ✅ No token size limits
      • ❌ Requires database query per request
      • ❌ Not compatible with all edge runtimes

    Decision Matrix:

    Use Case Recommended Strategy Reason
    Next.js App Router JWT Edge runtime compatibility
    Cloudflare Workers JWT or Database (with D1) D1 is edge-compatible
    Traditional Node.js Database Full session control
    High security Database Can invalidate sessions
    Read-only sessions JWT No database overhead

    See references/session-strategies.md for detailed comparison.


    Session Strategies

    JWT Sessions (Default)

    When to use:

    • Edge runtime deployment (Cloudflare Workers, Vercel Edge)
    • No database adapter configured
    • Read-only session data
    • High-traffic applications (no DB queries)

    Configuration:

    export const { handlers, auth } = NextAuth({
      session: {
        strategy: "jwt",
        maxAge: 30 * 24 * 60 * 60, // 30 days
      },
      providers: [GitHub],
    })
    

    ⚠️ CRITICAL: Even if you have an adapter configured, you MUST explicitly set strategy: "jwt" for edge runtime compatibility!

    Database Sessions

    When to use:

    • Node.js runtime (not edge)
    • Need to invalidate sessions server-side
    • Compliance requirements (audit trail)
    • Session data larger than JWT limits

    Configuration:

    import { PrismaAdapter } from "@auth/prisma-adapter"
    
    export const { handlers, auth } = NextAuth({
      adapter: PrismaAdapter(prisma),
      session: {
        strategy: "database",
        maxAge: 30 * 24 * 60 * 60,
        updateAge: 24 * 60 * 60, // Update session every 24 hours
      },
      providers: [GitHub],
    })
    

    ⚠️ Edge Runtime Limitation:

    If your adapter is NOT edge-compatible, you CANNOT use database sessions in edge runtime (middleware). Split your config:

    // auth.config.ts (edge-compatible)
    export default {
      providers: [GitHub],
    } satisfies NextAuthConfig
    
    // auth.ts (full config with database)
    import authConfig from "./auth.config"
    
    export const { handlers, auth } = NextAuth({
      adapter: PrismaAdapter(prisma), // Not edge-compatible
      session: { strategy: "jwt" }, // FORCE JWT for middleware
      ...authConfig,
    })
    

    Provider Setup

    OAuth Providers (GitHub, Google, etc.)

    GitHub:

    import GitHub from "next-auth/providers/github"
    
    export const { handlers, auth } = NextAuth({
      providers: [
        GitHub({
          clientId: process.env.AUTH_GITHUB_ID,
          clientSecret: process.env.AUTH_GITHUB_SECRET,
        }),
      ],
    })
    

    Google (with token refresh):

    import Google from "next-auth/providers/google"
    
    export const { handlers, auth } = NextAuth({
      providers: [
        Google({
          clientId: process.env.AUTH_GOOGLE_ID,
          clientSecret: process.env.AUTH_GOOGLE_SECRET,
          authorization: {
            params: {
              prompt: "consent",
              access_type: "offline",
              response_type: "code",
            },
          },
        }),
      ],
    })
    

    See templates/providers/oauth-github-google.ts for complete example.

    Credentials Provider (Email/Password)

    ⚠️ CRITICAL: The Credentials provider ONLY supports JWT sessions!

    import Credentials from "next-auth/providers/credentials"
    import { z } from "zod"
    import bcrypt from "bcryptjs"
    
    const signInSchema = z.object({
      email: z.string().email(),
      password: z.string().min(8),
    })
    
    export const { handlers, auth } = NextAuth({
      providers: [
        Credentials({
          credentials: {
            email: { label: "Email", type: "email" },
            password: { label: "Password", type: "password" },
          },
          authorize: async (credentials) => {
            try {
              const { email, password } = await signInSchema.parseAsync(credentials)
    
              // Fetch user from database
              const user = await db.user.findUnique({ where: { email } })
    
              if (!user || !user.password) {
                // CRITICAL: Return null, DON'T throw
                return null
              }
    
              const passwordMatch = await bcrypt.compare(password, user.password)
    
              if (!passwordMatch) {
                return null
              }
    
              // Return user object
              return {
                id: user.id,
                email: user.email,
                name: user.name,
              }
            } catch (error) {
              // CRITICAL: Return null on error, DON'T throw
              console.error("Auth error:", error)
              return null
            }
          },
        }),
      ],
    })
    

    ⚠️ CRITICAL RULES:

    1. NEVER throw errors in authorize() - always return null
    2. Why? Throwing errors causes CallbackRouteError instead of CredentialsSignin
    3. Always validate with Zod before checking credentials
    4. Hash passwords with bcrypt, never store plain text

    See templates/providers/credentials.ts for complete example.

    Magic Link Provider (Passwordless)

    ⚠️ CRITICAL: Magic links REQUIRE a database adapter!

    import Resend from "next-auth/providers/resend"
    import { PrismaAdapter } from "@auth/prisma-adapter"
    
    export const { handlers, auth } = NextAuth({
      adapter: PrismaAdapter(prisma), // REQUIRED
      providers: [
        Resend({
          apiKey: process.env.AUTH_RESEND_KEY,
          from: "noreply@example.com",
        }),
      ],
    })
    

    See templates/providers/magic-link-resend.ts for complete example.


    Database Adapters

    Cloudflare D1 Adapter

    Installation:

    npm install @auth/d1-adapter@latest
    

    Wrangler Configuration:

    // wrangler.jsonc
    {
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "auth_db",
          "database_id": "your-database-id"
        }
      ]
    }
    

    Usage:

    import { D1Adapter } from "@auth/d1-adapter"
    
    // Cloudflare Workers
    const app = new Hono<{ Bindings: { DB: D1Database } }>()
    
    app.all('/api/auth/*', async (c) => {
      return await Auth(c.req.raw, {
        adapter: D1Adapter(c.env.DB),
        providers: [GitHub],
      })
    })
    
    // Next.js (with D1 via wrangler)
    export const { handlers, auth } = NextAuth({
      adapter: D1Adapter(env.DB),
      session: { strategy: "jwt" }, // Recommended for edge
      providers: [GitHub],
    })
    

    Database Schema:

    See templates/cloudflare-workers/schema.sql for complete schema.

    Edge Compatibility: ✅ D1 is fully edge-compatible!

    Prisma Adapter (PostgreSQL/MySQL)

    Installation:

    npm install @auth/prisma-adapter@latest
    npm install @prisma/client@latest
    npm install -D prisma@latest
    

    Prisma Schema:

    // prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    model User {
      id            String    @id @default(cuid())
      name          String?
      email         String    @unique
      emailVerified DateTime?
      image         String?
      accounts      Account[]
      sessions      Session[]
    }
    
    model Account {
      id                String  @id @default(cuid())
      userId            String
      type              String
      provider          String
      providerAccountId String
      refresh_token     String? @db.Text
      access_token      String? @db.Text
      expires_at        Int?
      token_type        String?
      scope             String?
      id_token          String? @db.Text
      session_state     String?
    
      user User @relation(fields: [userId], references: [id], onDelete: Cascade)
    
      @@unique([provider, providerAccountId])
    }
    
    model Session {
      id           String   @id @default(cuid())
      sessionToken String   @unique
      userId       String
      expires      DateTime
      user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
    }
    
    model VerificationToken {
      identifier String
      token      String   @unique
      expires    DateTime
    
      @@unique([identifier, token])
    }
    

    Usage:

    import { PrismaAdapter } from "@auth/prisma-adapter"
    import { PrismaClient } from "@prisma/client"
    
    const prisma = new PrismaClient()
    
    export const { handlers, auth } = NextAuth({
      adapter: PrismaAdapter(prisma),
      session: { strategy: "jwt" }, // For edge compatibility
      providers: [GitHub],
    })
    

    Edge Compatibility: ❌ Prisma is NOT edge-compatible by default

    Workaround: Use Prisma Accelerate or Hyperdrive for edge compatibility, OR force JWT sessions and don't use the adapter in middleware.

    See templates/database-adapters/prisma-postgresql.ts for complete example.


    Middleware Patterns

    Session Keep-Alive

    // middleware.ts
    export { auth as middleware } from "@/auth"
    
    export const config = {
      matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
    }
    

    This automatically updates session expiry on every request.

    Route Protection

    // middleware.ts
    import { auth } from "@/auth"
    import { NextResponse } from "next/server"
    
    export default auth((req) => {
      const { pathname } = req.nextUrl
    
      // Protect /dashboard routes
      if (pathname.startsWith("/dashboard")) {
        if (!req.auth) {
          const loginUrl = new URL("/login", req.url)
          loginUrl.searchParams.set("callbackUrl", pathname)
          return NextResponse.redirect(loginUrl)
        }
      }
    
      return NextResponse.next()
    })
    
    export const config = {
      matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
    }
    

    Role-Based Access Control (RBAC)

    // auth.ts
    export const { handlers, auth } = NextAuth({
      providers: [GitHub],
      callbacks: {
        async jwt({ token, user }) {
          // Add role to JWT on sign in
          if (user) {
            token.role = user.role
          }
          return token
        },
        async session({ session, token }) {
          // Expose role to session
          if (session.user) {
            session.user.role = token.role
          }
          return session
        },
      },
    })
    
    // middleware.ts
    import { auth } from "@/auth"
    import { NextResponse } from "next/server"
    
    export default auth((req) => {
      const { pathname } = req.nextUrl
    
      // Protect /admin routes (require admin role)
      if (pathname.startsWith("/admin")) {
        if (!req.auth || req.auth.user.role !== "admin") {
          return NextResponse.redirect(new URL("/unauthorized", req.url))
        }
      }
    
      return NextResponse.next()
    })
    

    See references/middleware-patterns.md and templates/advanced/role-based-access.ts for complete examples.


    Advanced Features

    JWT Callbacks (Custom Claims)

    export const { handlers, auth } = NextAuth({
      providers: [GitHub],
      callbacks: {
        async jwt({ token, user, account }) {
          // On sign in
          if (user) {
            token.id = user.id
            token.role = user.role
          }
    
          // On subsequent requests
          return token
        },
        async session({ session, token }) {
          // Expose custom claims to session
          session.user.id = token.id
          session.user.role = token.role
          return session
        },
      },
    })
    

    Token Refresh (OAuth)

    Google OAuth with token refresh:

    import Google from "next-auth/providers/google"
    
    export const { handlers, auth } = NextAuth({
      providers: [
        Google({
          authorization: {
            params: {
              prompt: "consent",
              access_type: "offline",
              response_type: "code",
            },
          },
        }),
      ],
      callbacks: {
        async jwt({ token, account }) {
          // First-time login, save tokens
          if (account) {
            return {
              ...token,
              access_token: account.access_token,
              expires_at: account.expires_at,
              refresh_token: account.refresh_token,
            }
          }
    
          // Subsequent logins, check if token expired
          if (Date.now() < token.expires_at * 1000) {
            return token
          }
    
          // Token expired, refresh it
          try {
            const response = await fetch("https://oauth2.googleapis.com/token", {
              method: "POST",
              body: new URLSearchParams({
                client_id: process.env.AUTH_GOOGLE_ID!,
                client_secret: process.env.AUTH_GOOGLE_SECRET!,
                grant_type: "refresh_token",
                refresh_token: token.refresh_token!,
              }),
            })
    
            const newTokens = await response.json()
    
            return {
              ...token,
              access_token: newTokens.access_token,
              expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
              refresh_token: newTokens.refresh_token ?? token.refresh_token,
            }
          } catch (error) {
            console.error("Token refresh error:", error)
            token.error = "RefreshTokenError"
            return token
          }
        },
        async session({ session, token }) {
          session.error = token.error
          return session
        },
      },
    })
    

    See templates/advanced/jwt-refresh-tokens.ts for complete example.


    Critical Rules

    ✅ Always Do:

    1. Set AUTH_SECRET in production

      # Generate secret
      npx auth secret
      
      # Add to .env.local
      AUTH_SECRET=your-generated-secret
      
    2. Return null (not throw) in Credentials authorize()

      authorize: async (credentials) => {
        if (!validUser) {
          return null // ✅ Correct
          // throw new Error("Invalid") // ❌ WRONG - causes CallbackRouteError
        }
      }
      
    3. Use adapter for Magic Links

      export const { handlers, auth } = NextAuth({
        adapter: PrismaAdapter(prisma), // REQUIRED for magic links
        providers: [Resend],
      })
      
    4. Split config for edge compatibility

      // auth.config.ts (edge-compatible)
      export default { providers: [GitHub] }
      
      // auth.ts (full config with database)
      export const { handlers, auth } = NextAuth({
        adapter: PrismaAdapter(prisma),
        session: { strategy: "jwt" }, // Force JWT for edge
        ...authConfig,
      })
      
    5. Force JWT sessions when using non-edge adapter

      export const { handlers, auth } = NextAuth({
        adapter: PrismaAdapter(prisma), // Not edge-compatible
        session: { strategy: "jwt" }, // CRITICAL
      })
      

    ❌ Never Do:

    1. Deploy without AUTH_SECRET

      // WRONG - will fail in production
      // Missing AUTH_SECRET environment variable
      
    2. Throw errors in authorize()

      authorize: async (credentials) => {
        throw new Error("Invalid credentials") // ❌ Causes CallbackRouteError
      }
      
    3. Use deprecated @next-auth/ adapters*

      npm install @next-auth/prisma-adapter  # ❌ Deprecated
      npm install @auth/prisma-adapter       # ✅ Correct (v5)
      
    4. Use database session without edge-compatible adapter in middleware

      // WRONG - Prisma not edge-compatible
      export const { handlers, auth } = NextAuth({
        adapter: PrismaAdapter(prisma),
        session: { strategy: "database" }, // ❌ Fails in middleware
      })
      
    5. Mix v4 and v5 packages

      npm install next-auth@4.x @auth/core@0.x  # ❌ Version mismatch
      

    Common Errors & Fixes

    1. Missing AUTH_SECRET

    Error:

    JWEDecryptionFailed: Invalid secret
    

    Cause: Missing or incorrect AUTH_SECRET environment variable

    Fix:

    # Generate secret
    npx auth secret
    
    # Add to .env.local
    AUTH_SECRET=your-generated-secret
    
    # In production (Vercel, Cloudflare, etc.)
    # Add AUTH_SECRET as environment variable in dashboard
    

    2. CallbackRouteError

    Error:

    CallbackRouteError: Illegal arguments: string, undefined
    

    Cause: Throwing errors in authorize() callback

    Fix:

    // WRONG
    authorize: async (credentials) => {
      if (!user) throw new Error("Invalid credentials")
    }
    
    // CORRECT
    authorize: async (credentials) => {
      if (!user) return null
      return user
    }
    

    3. Route Not Found

    Error:

    Error: next-auth route not found
    

    Cause: Incorrect file path for API route handler

    Fix:

    // Ensure file is at EXACT path:
    // app/api/auth/[...nextauth]/route.ts
    
    import { handlers } from "@/auth"
    export const { GET, POST } = handlers
    

    4. Edge Compatibility Error

    Error:

    Module not compatible with edge runtime
    

    Cause: Using non-edge adapter with database session strategy

    Fix:

    // Option 1: Force JWT sessions
    export const { handlers, auth } = NextAuth({
      adapter: PrismaAdapter(prisma),
      session: { strategy: "jwt" }, // ✅
    })
    
    // Option 2: Use edge-compatible adapter (D1)
    export const { handlers, auth } = NextAuth({
      adapter: D1Adapter(env.DB), // ✅ Edge-compatible
      session: { strategy: "database" },
    })
    

    5. Session Not Updating

    Error: Session expires but doesn't refresh

    Cause: Missing middleware

    Fix:

    // middleware.ts
    export { auth as middleware } from "@/auth"
    
    export const config = {
      matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
    }
    

    6-12. More Errors

    See references/common-errors.md for complete list of all 12 documented errors with fixes.


    Templates Reference

    All templates are available in the templates/ directory:

    Next.js Templates (5)

    • auth.ts - Simple Next.js config (JWT sessions)
    • auth.config.ts + auth.ts - Split config (edge-compatible)
    • multi-provider.ts - OAuth + Credentials + Magic Links
    • middleware.ts - Session keep-alive + route protection
    • package.json + .env.example - Dependencies + environment

    Cloudflare Workers Templates (3)

    • worker-hono-auth.ts - Complete Hono + Auth.js + D1
    • wrangler.jsonc - D1 binding configuration
    • schema.sql - D1 tables for Auth.js

    Provider Templates (3)

    • oauth-github-google.ts - OAuth setup
    • credentials.ts - Email/password with Zod validation
    • magic-link-resend.ts - Passwordless email auth

    Advanced Templates (2)

    • jwt-refresh-tokens.ts - Google token rotation example
    • role-based-access.ts - RBAC with custom claims

    Copy these files to your project and customize as needed.


    Package Versions

    Current (as of 2025-10-26):

    {
      "next-auth": "4.24.11",
      "@auth/core": "0.41.1",
      "@auth/d1-adapter": "1.11.1",
      "@auth/prisma-adapter": "2.7.5",
      "next": "15.3.1",
      "hono": "4.7.9"
    }
    

    Always use latest versions:

    npm install next-auth@latest @auth/core@latest
    

    Official Documentation

    • Auth.js Docs: https://authjs.dev
    • Getting Started: https://authjs.dev/getting-started/installation
    • Providers: https://authjs.dev/getting-started/providers/oauth
    • Adapters: https://authjs.dev/getting-started/adapters
    • D1 Adapter: https://authjs.dev/getting-started/adapters/d1
    • v5 Migration: https://authjs.dev/getting-started/migrating-to-v5
    • Edge Compatibility: https://authjs.dev/guides/edge-compatibility

    Reference Documentation

    For deeper understanding, see:

    • common-errors.md - All 12 documented errors and fixes
    • edge-compatibility.md - Edge runtime compatibility matrix
    • v5-migration-guide.md - v4 → v5 breaking changes
    • session-strategies.md - JWT vs Database comparison
    • middleware-patterns.md - Route protection, RBAC
    • jwt-customization.md - Custom claims, token refresh
    • provider-setup-guides.md - OAuth app setup instructions

    Questions? Issues?

    1. Check references/common-errors.md first
    2. Verify AUTH_SECRET is set
    3. Ensure you're not throwing in authorize()
    4. Check edge compatibility for your adapter
    5. Review official docs: https://authjs.dev
    Recommended Servers
    OpenZeppelin
    OpenZeppelin
    MantleKit Launch Planner
    MantleKit Launch Planner
    StudioMeyer-Crew
    StudioMeyer-Crew
    Repository
    jackspace/claudeskillz
    Files