Setting up authentication in Next.js 14 (App Router) is a nightmare if you don't understand the boundary between Server Components and Client Components.

I spent hours debugging useSession returning null or throwing hydration errors. Here is the exact, fool-proof way to set up NextAuth (Auth.js) with a MongoDB database.

1. Installation

 bash npm install next-auth @auth/mongodb-adapter mongodb 2. Environment Variables Create a .env.local file and add your GitHub OAuth credentials and MongoDB URI: env GITHUB_ID=your_github_client_id GITHUB_SECRET=your_github_client_secret MONGODB_URI=your_mongodb_connection_string NEXTAUTH_SECRET=any_long_random_string NEXTAUTH_URL=http://localhost:3000 3. The MongoDB Adapter To save users in your database, create a file lib/mongodb.js: javascript import { MongoClient } from "mongodb"; if (!process.env.MONGODB_URI) { throw new Error('Invalid/Missing environment variable: "MONGODB_URI"'); } const uri = process.env.MONGODB_URI; const options = {}; let client; let clientPromise; if (process.env.NODE_ENV === "development") { if (!global._mongoClientPromise) { client = new MongoClient(uri, options); global._mongoClientPromise = client.connect(); } clientPromise = global._mongoClientPromise; } else { client = new MongoClient(uri, options); clientPromise = client.connect(); } export default clientPromise; 4. The Auth Route Handler In App Router, NextAuth requires a specific Route Handler. Create app/api/auth/[...nextauth]/route.js: javascript import NextAuth from "next-auth"; import GithubProvider from "next-auth/providers/github"; import { MongoDBAdapter } from "@auth/mongodb-adapter"; import clientPromise from "@/lib/mongodb"; export const authOptions = { adapter: MongoDBAdapter(clientPromise), providers: [ GithubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), ], secret: process.env.NEXTAUTH_SECRET, session: { strategy: "jwt", }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; 5. The Client Component Wrapper Because layout.js is a Server Component by default, you cannot put SessionProvider directly inside it. You need a wrapper. Create app/providers.js: javascript "use client"; import { SessionProvider } from "next-auth/react"; export default function Providers({ children }) { return <SessionProvider>{children}</SessionProvider>; } Now wrap your body in app/layout.js: javascript import Providers from "./providers"; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); } 6. Using the Session (The Trap) This is where 90% of people fail. In Client Components: javascript "use client"; import { useSession } from "next-auth/react"; export default function Profile() { const { data: session } = useSession(); if (!session) return <p>Not logged in</p>; return <p>Welcome {session.user.name}</p>; } In Server Components / API Routes: Never use useSession here. Use getServerSession. javascript import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; export default async function ServerPage() { const session = await getServerSession(authOptions); if (!session) return <p>Unauthorized</p>; return <p>Hello {session.user.email}</p>; } Conclusion NextAuth is incredibly powerful once it clicks. The golden rule of App Router: Keep hooks in the client, and getServerSession in the server. Stop wasting hours on hydration errors. Build.