Auth.js vs BetterAuth: The Ultimate Comparison Guide for Next.js

AndryAndry Dina
92
Auth.js vs BetterAuth

Auth.js vs BetterAuth: Which Authentication Library Is Best for Next.js in 2025?

Authentication can make or break your web app’s onboarding experience. Choosing the wrong auth solution can lead to frustrating bugs, security holes, or wasted weeks of refactoring.

In this guide, we’ll compare Auth.js and BetterAuth — two of the most popular libraries for adding authentication to your Next.js app.


TL;DR

FeatureAuth.jsBetterAuth
Setup Time⏱️ Medium⚡ Fast
Documentation📖 Complex but thorough🧼 Clear & minimal
Flexibility🧩 Very high🔒 Opinionated
MFA Support🔌 Optional✅ Built-in
Session Strategy🔄 JWT/Database🔐 JWT only
Suitable for Teams?✅ Yes🤔 Limited
TypeScript DX😵‍💫 Inconsistent🧠 Strong inference

When to Use Each

✅ Use Auth.js if:

  • You need maximum flexibility (e.g. custom flows, adapters)
  • You’re using multiple OAuth providers
  • You’re comfortable digging into internals

✅ Use BetterAuth if:

  • You want a fast and secure default config
  • You prefer simple patterns over flexibility
  • You want MFA out of the box

Setup Experience

🔧 Auth.js

Auth.js requires defining multiple layers of config. Here’s a basic setup:

ts
1// app/api/auth/[...nextauth]/route.ts
2
3import NextAuth from "next-auth";
4import GitHub from "next-auth/providers/github";
5
6const handler = NextAuth({
7  providers: [GitHub],
8  callbacks: {
9    session: ({ session, token }) => {
10      session.user.id = token.sub;
11      return session;
12    },
13  },
14});
15
16export { handler as GET, handler as POST };

Expect to write more boilerplate for TypeScript types, server/client sync, and session management.

⚡ BetterAuth

BetterAuth ships with sensible defaults and a simplified API:

ts
1// app/api/auth/route.ts
2
3import { authHandler } from "betterauth/server";
4import GitHub from "betterauth/providers/github";
5
6export const { GET, POST } = authHandler({
7  providers: [GitHub()],
8  features: { mfa: true },
9});

Cleaner syntax, no extra adapter config, and MFA support enabled by default.


Developer Experience

Auth.js has a steeper learning curve. Docs are comprehensive, but overwhelming. You’ll often end up in Discord or GitHub issues.

BetterAuth’s DX is beginner-friendly. It uses generics and zod under the hood, so type safety “just works.”

💡 Pro tip: BetterAuth auto-inflects types from your session and provider setup — no need for a next-auth.d.ts dance.


Implementation Comparison

Let's look at how both solutions handle common authentication scenarios.

Setting Up Auth.js

First, install the necessary packages:

bash
1npm install next-auth
2# or
3yarn add next-auth

Create an API route for authentication:

ts
1// pages/api/auth/[...nextauth].ts
2import NextAuth from "next-auth"
3import CredentialsProvider from "next-auth/providers/credentials"
4
5export default NextAuth({
6  providers: [
7    CredentialsProvider({
8      name: "Credentials",
9      credentials: {
10        username: { label: "Username", type: "text" },
11        password: { label: "Password", type: "password" }
12      },
13      async authorize(credentials, req) {
14        // Add your authentication logic here
15        const user = await authenticateUser(credentials)
16        if (user) {
17          return user
18        }
19        return null
20      }
21    })
22  ],
23  session: {
24    strategy: "jwt"
25  },
26  callbacks: {
27    async session({ session, token }) {
28      // Add custom session handling
29      return session
30    }
31  }
32})

Set up the provider in your app:

tsx
1// pages/_app.tsx
2import { SessionProvider } from "next-auth/react"
3
4function MyApp({ Component, pageProps: { session, ...pageProps } }) {
5  return (
6    <SessionProvider session={session}>
7      <Component {...pageProps} />
8    </SessionProvider>
9  )
10}
11
12export default MyApp

Setting Up BetterAuth

Install BetterAuth:

bash
1npm install better-auth
2# or
3yarn add better-auth

Create your auth configuration:

ts
1// lib/auth.ts
2import { createAuth } from "better-auth"
3
4export const auth = createAuth({
5  secret: process.env.AUTH_SECRET,
6  database: {
7    type: "postgres",
8    url: process.env.DATABASE_URL
9  },
10  session: {
11    strategy: "jwt"
12  }
13})

Set up the API route:

ts
1// pages/api/auth/[...auth].ts
2import { auth } from "@/lib/auth"
3import { createHandler } from "better-auth/next"
4
5export default createHandler(auth)

Real-World Implementation Examples

Let's explore some common authentication scenarios and how they're handled in both libraries.

User Login with Auth.js

tsx
1// components/LoginForm.tsx
2import { signIn } from "next-auth/react"
3import { useState } from "react"
4
5export default function LoginForm() {
6  const [credentials, setCredentials] = useState({
7    email: "",
8    password: ""
9  })
10
11  const handleSubmit = async (e) => {
12    e.preventDefault()
13    try {
14      const result = await signIn("credentials", {
15        redirect: false,
16        email: credentials.email,
17        password: credentials.password
18      })
19      
20      if (result?.error) {
21        // Handle error
22        console.error(result.error)
23      }
24    } catch (error) {
25      console.error("Login failed:", error)
26    }
27  }
28
29  return (
30    <form onSubmit={handleSubmit}>
31      <input
32        type="email"
33        value={credentials.email}
34        onChange={(e) => setCredentials({
35          ...credentials,
36          email: e.target.value
37        })}
38      />
39      <input
40        type="password"
41        value={credentials.password}
42        onChange={(e) => setCredentials({
43          ...credentials,
44          password: e.target.value
45        })}
46      />
47      <button type="submit">Login</button>
48    </form>
49  )
50}

User Login with BetterAuth

tsx
1// components/LoginForm.tsx
2import { useAuth } from "better-auth/react"
3import { useState } from "react"
4
5export default function LoginForm() {
6  const { login } = useAuth()
7  const [credentials, setCredentials] = useState({
8    email: "",
9    password: ""
10  })
11
12  const handleSubmit = async (e) => {
13    e.preventDefault()
14    try {
15      await login({
16        email: credentials.email,
17        password: credentials.password
18      })
19      // Successful login will automatically update auth state
20    } catch (error) {
21      console.error("Login failed:", error)
22    }
23  }
24
25  return (
26    <form onSubmit={handleSubmit}>
27      <input
28        type="email"
29        value={credentials.email}
30        onChange={(e) => setCredentials({
31          ...credentials,
32          email: e.target.value
33        })}
34      />
35      <input
36        type="password"
37        value={credentials.password}
38        onChange={(e) => setCredentials({
39          ...credentials,
40          password: e.target.value
41        })}
42      />
43      <button type="submit">Login</button>
44    </form>
45  )
46}

Protected Routes and Session Management

Auth.js Protected Routes

ts
1// middleware.ts
2export { default } from "next-auth/middleware"
3
4export const config = {
5  matcher: ["/protected/:path*"]
6}
tsx
1// pages/protected/dashboard.tsx
2import { useSession } from "next-auth/react"
3import { useRouter } from "next/router"
4
5export default function Dashboard() {
6  const { data: session, status } = useSession()
7  const router = useRouter()
8
9  if (status === "loading") {
10    return <div>Loading...</div>
11  }
12
13  if (!session) {
14    router.push("/login")
15    return null
16  }
17
18  return (
19    <div>
20      <h1>Welcome {session.user.name}</h1>
21      {/* Dashboard content */}
22    </div>
23  )
24}

BetterAuth Protected Routes

ts
1// middleware.ts
2import { createMiddleware } from "better-auth/next"
3import { auth } from "@/lib/auth"
4
5export default createMiddleware(auth)
6
7export const config = {
8  matcher: ["/protected/:path*"]
9}
tsx
1// pages/protected/dashboard.tsx
2import { useAuth } from "better-auth/react"
3
4export default function Dashboard() {
5  const { user, isLoading } = useAuth()
6
7  if (isLoading) {
8    return <div>Loading...</div>
9  }
10
11  return (
12    <div>
13      <h1>Welcome {user.name}</h1>
14      {/* Dashboard content */}
15    </div>
16  )
17}

Performance and Security Considerations

Auth.js Security Features

JWT Handling:

ts
1// pages/api/auth/[...nextauth].ts
2export default NextAuth({
3  jwt: {
4    secret: process.env.JWT_SECRET,
5    maxAge: 60 * 60 * 24 * 30, // 30 days
6    encryption: true
7  },
8  security: {
9    csrf: true,
10    cookieSecure: process.env.NODE_ENV === "production"
11  }
12})

Custom Authorization Logic:

ts
1// lib/auth-checks.ts
2export const checkUserPermissions = async (session) => {
3  if (!session) return false
4  
5  // Add your custom authorization logic
6  const userRoles = await fetchUserRoles(session.user.id)
7  return userRoles.includes("admin")
8}

BetterAuth Security Features

Built-in Rate Limiting:

ts
1// lib/auth.ts
2import { createAuth } from "better-auth"
3
4export const auth = createAuth({
5  rateLimit: {
6    enabled: true,
7    windowMs: 15 * 60 * 1000, // 15 minutes
8    max: 100 // limit each IP to 100 requests per windowMs
9  },
10  security: {
11    passwordPolicy: {
12      minLength: 8,
13      requireNumbers: true,
14      requireSpecialChars: true
15    }
16  }
17})

MFA Implementation:

ts
1// pages/api/auth/enable-mfa.ts
2import { auth } from "@/lib/auth"
3
4export default async function handler(req, res) {
5  const { user } = await auth.getUserSession(req)
6  
7  if (!user) {
8    return res.status(401).json({ error: "Unauthorized" })
9  }
10
11  const secret = await auth.mfa.generateSecret()
12  const qrCode = await auth.mfa.generateQRCode(secret)
13
14  return res.json({ secret, qrCode })
15}

Migration Considerations

If you're considering migrating from Auth.js to BetterAuth, here's a step-by-step approach:

  1. Backup Your Data
bash
1# Export your users and sessions
2pg_dump -t users -t sessions > auth_backup.sql
  1. Update Dependencies
bash
1npm remove next-auth
2npm install better-auth
  1. Update Configuration
ts
1// Before (Auth.js)
2export default NextAuth({
3  providers: [...],
4  callbacks: {...},
5  session: {...}
6})
7
8// After (BetterAuth)
9export const auth = createAuth({
10  providers: {...},
11  session: {...},
12  callbacks: {...}
13})
  1. Update Components
ts
1// Before (Auth.js)
2import { useSession } from "next-auth/react"
3
4// After (BetterAuth)
5import { useAuth } from "better-auth/react"

Real-World Migration Example

Let's say you're moving from Auth.js to BetterAuth.

  1. Replace API route: Change the handler in app/api/auth/[...nextauth] to app/api/auth/route.ts
  2. Update client hooks: Replace useSession from next-auth/react with useBetterSession()
  3. Remove adapters & JWT callbacks — BetterAuth handles this internally.
  4. Add MFA (optional) via config flag
ts
1// hooks/useSession.ts
2
3import { useSession } from "betterauth/client";
4
5export function useUser() {
6  const { data } = useSession();
7  return data?.user;
8}

Migration usually takes 1–2 hours for a typical project.


FAQs

❓ Is BetterAuth production-ready?

Yes. It's used in multiple SaaS products and audited by third-party security firms.

❓ Can I use BetterAuth with Next.js App Router?

Absolutely — it was designed with App Router in mind.

❓ Does Auth.js support MFA?

Not out of the box. You'll need to implement your own flow or use a plugin.


Verdict

Go with BetterAuth if you want speed, simplicity, and strong security out of the box.

Stick with Auth.js if you need advanced customizations or already have a large app using it.

No matter your choice, both tools can serve you well in 2025 — it just depends on your project's complexity and your team's preferences.


Related Resources


Got questions or want a migration walkthrough? Contact us or drop a comment below!

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.