mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmSecurityupdated 05 Aug 2026

next-auth

Authentication for Next.js apps, now part of the wider Auth.js project. It handles the OAuth dance for dozens of built-in providers (Google, GitHub, and many more), plus email magic links, passkeys/WebAuthn, and encrypted JWT or database-backed sessions, with CSRF protection and sane cookie defaults out of the box. You mount one API route, wrap your app in a session provider, and get signIn/signOut/useSession. Important context: the project's own README now says Auth.js has joined Better Auth and recommends new projects start with Better Auth unless they specifically need stateless, database-free sessions.

Verdict

Still fine to keep running in existing Next.js apps, and the fastest free path to OAuth sessions on v4. But with v5 perpetually in beta and the maintainers pointing new projects at Better Auth, starting fresh on next-auth today is choosing a library on its way out.

API stability2/5v4 itself is stable, but the ecosystem is split: v5 has been in beta for years with different config, env var names, and imports, and the project's future now runs through Better Auth
Docs3/5authjs.dev is extensive with per-provider and per-adapter pages, but it primarily documents the v5/Auth.js API while npm latest is v4, and version mismatch is the top source of user confusion
Maintenance3/5Latest release July 2026 and pushes in the last two weeks, so not abandoned; but v5 never left beta, 591 issues and PRs are open, and the team's energy has publicly moved to Better Auth
Ecosystem4/55.7M weekly downloads, 28k stars, official adapters for most databases (MySQL, Postgres, MongoDB, SQLite, and more), and years of community answers; new ecosystem growth is heading to Better Auth

Use it if

  • You need OAuth sign-in (Google, GitHub, etc.) in an existing Next.js app quickly, with CSRF and cookie handling done for you
  • You want stateless JWT sessions with no database at all; encrypted JWTs are the default and this remains the feature the maintainers say Better Auth lacks
  • You are on a Next.js codebase that already uses next-auth and needs providers added or sessions extended, where migrating auth is not on the roadmap
  • You want to own your auth and user data on your own infrastructure instead of paying a hosted provider like Clerk or Auth0
Skip it if

Setup reality

Install is one package, but configuration is where the time goes: a [...nextauth] catch-all route, provider client IDs and secrets from each OAuth console, NEXTAUTH_SECRET and NEXTAUTH_URL env vars (deployments break in confusing ways when these are wrong), and a SessionProvider wrapper for client components. The real tax is version confusion: v4 (stable, pages- and app-router capable) and v5 beta (auth() everywhere, different config file, AUTH_* env names) coexist, docs at authjs.dev lean v5 while npm installs v4, so copy-pasted snippets routinely target the wrong major. Database sessions add an adapter package and schema migrations on top.

Patterns

Set up next-auth v4 with an OAuth providerbasic-setup

// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';

export const authOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
};

export default NextAuth(authOptions);

Also set NEXTAUTH_SECRET (and NEXTAUTH_URL outside Vercel) or production logins fail with JWT decryption errors.

Mount the v4 handler in the App Routerapp-router-setup

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

Keep authOptions in a separate file so getServerSession can import it; exporting it from route.ts breaks Next.js route type checks.

Read the session in a client componentclient-session

'use client';
import { useSession, signIn, signOut } from 'next-auth/react';

export function UserButton() {
  const { data: session, status } = useSession();
  if (status === 'loading') return <span>...</span>;
  if (!session) return <button onClick={() => signIn('github')}>Sign in</button>;
  return <button onClick={() => signOut()}>Sign out {session.user?.email}</button>;
}

useSession only works under a <SessionProvider>, which must live in a client component wrapper in the App Router.

Read the session on the serverserver-session

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export default async function Page() {
  const session = await getServerSession(authOptions);
  if (!session) return <p>Not signed in</p>;
  return <p>Hello {session.user?.name}</p>;
}

Always pass authOptions; calling getServerSession without it silently loses your callbacks and custom session shape.

Protect routes with middlewareprotect-middleware

// middleware.ts
export { default } from 'next-auth/middleware';

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};

The bundled middleware only works with JWT session strategy, not database sessions.

Email/password with the Credentials providercredentials-provider

import CredentialsProvider from 'next-auth/providers/credentials';

CredentialsProvider({
  credentials: {
    email: { label: 'Email', type: 'email' },
    password: { label: 'Password', type: 'password' },
  },
  async authorize(credentials) {
    const user = await verifyUser(credentials!.email, credentials!.password);
    return user ?? null; // null -> sign-in rejected
  },
});

You must supply hashing, rate limiting, and registration yourself; Credentials also forces JWT sessions even with a database adapter.

Add the user id to the sessionextend-session

export const authOptions = {
  // ...providers,
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.id = user.id;
      return token;
    },
    async session({ session, token }) {
      (session.user as any).id = token.id;
      return session;
    },
  },
};

The jwt callback runs first and only gets user on initial sign-in; persist anything you need onto the token there.

Persist users with a database adapterdatabase-adapter

import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'database' },
  // ...providers,
};

Adapters live in separate @auth/* packages and expect a specific schema (users, accounts, sessions, verification tokens); copy it from the adapter docs exactly.

Use your own sign-in pagecustom-signin-page

export const authOptions = {
  // ...providers,
  pages: {
    signIn: '/login',
    error: '/login', // OAuth errors land here as ?error=
  },
};

Your page must call signIn(provider) itself; check the error query param to surface OAuth failures to users.

Sign in and control the redirectsignin-redirect

import { signIn } from 'next-auth/react';

await signIn('google', { callbackUrl: '/dashboard' });

// credentials without a full redirect:
const res = await signIn('credentials', {
  redirect: false,
  email, password,
});
if (res?.error) setError('Invalid login');

callbackUrl must be same-origin or whitelisted in the redirect callback, otherwise next-auth falls back to the base URL.

Verify the JWT in an API routeget-token-api-route

import { getToken } from 'next-auth/jwt';

export default async function handler(req, res) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  if (!token) return res.status(401).end();
  res.json({ userId: token.id });
}

getToken reads and decrypts the session cookie directly; cheaper than getServerSession when you only need claims.

Guard a server actionprotect-server-action

'use server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function deletePost(id: string) {
  const session = await getServerSession(authOptions);
  if (!session) throw new Error('Unauthorized');
  await db.delete(posts).where(eq(posts.id, id));
}

Middleware does not cover server actions invoked from cached pages; check the session inside every mutating action.

Alternatives

PackageRegistryPick it when
better-authnpmNew projects; the Auth.js README itself now points here, and it ships email/password, organizations, and 2FA in the box
@clerk/nextjsnpmYou will pay for hosted auth to get polished prebuilt UI, user management, and orgs without building them
iron-sessionnpmYou just need small encrypted cookie sessions and want to wire up OAuth or passwords yourself
lucianpmYou want to learn how sessions actually work and hand-roll auth; note it is now a learning resource rather than a maintained package