Migrating from Auth.js to Better Auth: A Step-by-Step Guide


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
Thinking about switching authentication providers in your Next.js app? Better Auth offers more flexibility, cleaner APIs, and faster performance. Here's a complete step-by-step guide to migrating from Auth.js to Better Authβwithout losing data or breaking your app.
Before diving into the migration steps, here's why many teams are making the switch:
If you haven't already, install Better Auth in your project:
npm install better-auth
# or
yarn add better-auth
# or
pnpm add better-authThen set up the core configuration in your auth.ts file:
// server/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { nextCookies } from "better-auth/nextjs";
const prisma = new PrismaClient();
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
emailAndPassword: {
enabled: true,
},
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
plugins: [nextCookies()],
});Make sure to set the BETTER_AUTH_SECRET environment variable in your .env file. This should be a strong, random string (at least 32 characters).
One of the biggest advantages of Better Auth is that you don't need to rename your database fields. Instead, you can map your existing Auth.js schema to Better Auth's expected structure.
| Auth.js Field | Better Auth Field | Notes |
|---|---|---|
| emailVerified (v4) | emailVerified | change datetime β boolean |
| Auth.js Field | Better Auth Field | Notes |
|---|---|---|
| expires | expiresAt | |
| sessionToken | token | |
| (Add) | createdAt, updatedAt | fields (datetime) |
In your auth.ts configuration:
export const auth = betterAuth({
// Other configs
session: {
fields: {
expiresAt: "expires",
token: "sessionToken",
},
},
});| Auth.js Field | Better Auth Field | Notes |
|---|---|---|
| provider (v4) | providerId | |
| providerAccountId | accountId | |
| refresh_token | refreshToken | |
| access_token | accessToken | |
| access_token_expires | accessTokenExpiresAt | |
| expires_at (v4) | accessTokenExpiresAt | |
| id_token | idToken | |
| (Remove) | session_state, type, token_type | Not required by Better Auth |
| (Add) | createdAt, updatedAt | fields (datetime) |
In your auth.ts configuration:
export const auth = betterAuth({
// Other configs
account: {
fields: {
providerId: "provider", // For Auth.js v4
accountId: "providerAccountId",
refreshToken: "refresh_token",
accessToken: "access_token",
accessTokenExpiresAt: "expires_at", // For Auth.js v4
idToken: "id_token",
},
},
});If you're using Prisma, you can keep your existing schema and use the @map() directive to map the fields:
model Session {
id String @id @default(cuid())
expiresAt DateTime @map("expires")
token String @unique @map("sessionToken")
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
model Account {
id String @id @default(cuid())
userId String
providerId String @map("provider")
accountId String @map("providerAccountId")
refreshToken String? @map("refresh_token")
accessToken String? @map("access_token")
accessTokenExpiresAt DateTime? @map("expires_at")
idToken String? @map("id_token")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, accountId])
@@index([userId])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified Boolean? @default(false)
image String?
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}After updating your schema, run a migration:
npx prisma migrate dev --name better-auth-migrationIn your app/api/auth directory, rename the [...nextauth] folder to [...all] to avoid confusion. Then, update the route.ts file:
// app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "~/server/auth";
export const { POST, GET } = toNextJsHandler(auth);This handler will process all authentication-related requests, including sign-in, sign-out, and session management.
Create a reusable auth client file to centralize your authentication hooks and functions:
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BASE_URL, // Optional if your API is on the same domain
});Update your social login functions to use Better Auth's type-safe API:
import { authClient } from "@/lib/auth-client";
export const signInGoogle = async () => {
const { data, error } = authClient.signIn.social({
provider: "google",
callbackUrl: "/dashboard", // Optional redirect after successful login
});
if (error) {
console.error("Failed to sign in:", error.message);
return null;
}
// Success! User is now logged in
return data;
};To enable email and password authentication, you need to set the emailAndPassword.enabled option to true in the auth configuration.
// @/lib/auth.ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
});"use server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export const signInWithEmail = async (email: string, password: string) => {
try {
await auth.api.signInEmail({
body: {
email,
password,
},
});
return {
success: true,
message: "Signed in successfully.",
};
} catch (error) {
const e = error as Error;
return {
success: false,
message: e.message || "An unknown error occurred.",
};
}
};import { authClient } from "@/lib/auth-client";
export const signInWithEmail = async (email: string, password: string) => {
const { data, error } = authClient.signIn.email({
email,
password,
callbackUrl: "/dashboard", // Optional redirect after successful login
});
if (error) {
return { success: false, message: error.message };
}
return { success: true, data };
};Replace Auth.js's useSession hook with Better Auth's version:
"use client"
import { createAuthClient } from "better-auth/react"
const { useSession } = createAuthClient()
export const Profile = () => {
const { data: session, isPending, error } = useSession();
if (isPending) return <div>Loading user information...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!session) return <div>Please sign in to view your profile</div>;
return (
<div>
<h1>Welcome, {session.user.name || 'User'}</h1>
<img src={session.user.image || '/default-avatar.png'} alt="Profile" />
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
);
};For server components or server actions, use the auth instance to get session data:
// Server Component
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export default async function Dashboard() {
const session = await auth.api.getSession({
headers: headers(),
});
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
{/* Dashboard content */}
</div>
);
}// Server Action
"use server";
import { auth } from "~/server/auth";
import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
export const updateUserProfile = async (formData: FormData) => {
const session = await auth.api.getSession({
headers: headers(),
});
if (!session) {
throw new Error("Unauthorized");
}
const updateProfile = await prisma.user.update({
where: {
id: session.user.id,
},
data: {
name: formData.get("name") as string,
email: formData.get("email") as string,
},
});
revalidatePath("/profile");
return { success: true };
};// app/profile/page.tsx
import {updateUserProfile} from "@/server/actions";
export default function Profile() {
return (
<div>
<h1>Profile</h1>
<form action={updateUserProfile}>
<input type="text" name="name" />
<input type="email" name="email" />
<button type="submit">Update Profile</button>
</form>
</div>
);
}To protect routes with middleware, create a middleware.ts file in your project root:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
// Redirect unauthenticated users to login page
if (!sessionCookie && !request.nextUrl.pathname.startsWith("/login")) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// Optional: Role-based access control
if (
sessionCookie &&
request.nextUrl.pathname.startsWith("/admin") &&
sessionCookie.user.role !== "admin"
) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*", "/admin/:path*"],
};| Task | Status | Notes |
|---|---|---|
| Install Better Auth | β | npm install better-auth |
| Update auth configuration | β | Map fields to preserve existing data |
| Update Prisma schema | β | Use @map() directives |
| Update route handler | β | Rename [...nextauth] to [...all] |
| Create auth client | β | Export hooks and functions |
| Update client components | β | Replace useSession and signIn calls |
| Update server components | β | Use auth.api.getSession |
| Configure middleware | β | Protect routes as needed |
You've successfully migrated from Auth.js to Better Auth! Your application now benefits from:
All of this without losing any user data or breaking existing sessions.
Want to explore more? Check out the Better Auth docs or clone the demo repository for complete examples.
Q: Does this migration break existing user sessions?
A: Noβif you map your fields properly, existing users will remain logged in throughout and after the migration.
Q: Can I use this with server actions and server components?
A: Yes. Better Auth works seamlessly with Next.js 14/15 features including server components, server actions, and middleware.
Q: What about OAuth providers like Google/GitHub/Discord?
A: They're fully supported via the .social() method. Better Auth simplifies OAuth integration with improved error handling.
Q: Do I need to change my database schema?
A: No. You can map existing fields without renaming them, allowing for a smooth migration without data loss.
Q: How do I handle custom fields in my user model?
A: Better Auth supports custom fields through the user.fields configuration option, giving you full control over your data model.