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

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.
π TL;DR
- β Fully migrate from Auth.js to Better Auth without database schema changes
- π Map user, session, and account schemas with field mapping (no need to rename columns)
- π Update route handlers and session logic for Next.js App Router
- π¦ Replace useSession and signIn with Better Auth's type-safe alternatives
- π‘ Keep all existing user sessions active during and after migration
π§ Why Migrate to Better Auth?
Before diving into the migration steps, here's why many teams are making the switch:
- Type-safe APIs with full TypeScript support and better developer experience
- Granular control over authentication flow, session handling, and database schema
- Significantly improved performance in large-scale applications
- Built-in support for modern auth flows including passwordless, multi-provider login, and MFA
- Simplified OAuth integration with major providers like Google, GitHub, and Discord
π Step 1: Install Better Auth
If you haven't already, install Better Auth in your project:
1npm install better-auth
2# or
3yarn add better-auth
4# or
5pnpm add better-auth
Then set up the core configuration in your auth.ts file:
1// server/auth.ts
2import { betterAuth } from "better-auth";
3import { prismaAdapter } from "better-auth/adapters/prisma";
4import { PrismaClient } from "@prisma/client";
5
6const prisma = new PrismaClient();
7
8export const auth = betterAuth({
9 secret: process.env.BETTER_AUTH_SECRET,
10 database: prismaAdapter(prisma),
11 // Add your authentication providers here
12});
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).
π§± Step 2: Map Your Database Schema
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.
π€ User Schema Mapping
Auth.js Field | Better Auth Field | Notes |
---|---|---|
emailVerified (v4) | emailVerified | change datetime β boolean |
π Session Schema Mapping
Auth.js Field | Better Auth Field | Notes |
---|---|---|
expires | expiresAt | |
sessionToken | token | |
(Add) | createdAt, updatedAt | fields (datetime) |
In your auth.ts configuration:
1export const auth = betterAuth({
2 // Other configs
3 session: {
4 fields: {
5 expiresAt: "expires",
6 token: "sessionToken"
7 }
8 },
9});
π§Ύ Account Schema Mapping
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:
1export const auth = betterAuth({
2 // Other configs
3 account: {
4 fields: {
5 providerId: "provider", // For Auth.js v4
6 accountId: "providerAccountId",
7 refreshToken: "refresh_token",
8 accessToken: "access_token",
9 accessTokenExpiresAt: "expires_at", // For Auth.js v4
10 idToken: "id_token"
11 }
12 },
13});
π Step 3: Update Your Prisma Schema
If you're using Prisma, you can keep your existing schema and use the @map()
directive to map the fields:
1model Session {
2 id String @id @default(cuid())
3 expiresAt DateTime @map("expires")
4 token String @unique @map("sessionToken")
5 userId String
6 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
7 createdAt DateTime @default(now())
8 updatedAt DateTime @updatedAt
9
10 @@index([userId])
11}
12
13model Account {
14 id String @id @default(cuid())
15 userId String
16 providerId String @map("provider")
17 accountId String @map("providerAccountId")
18 refreshToken String? @map("refresh_token")
19 accessToken String? @map("access_token")
20 accessTokenExpiresAt DateTime? @map("expires_at")
21 idToken String? @map("id_token")
22 createdAt DateTime @default(now())
23 updatedAt DateTime @updatedAt
24 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
25
26 @@unique([providerId, accountId])
27 @@index([userId])
28}
29
30model User {
31 id String @id @default(cuid())
32 name String?
33 email String? @unique
34 emailVerified Boolean? @default(false)
35 image String?
36 accounts Account[]
37 sessions Session[]
38 createdAt DateTime @default(now())
39 updatedAt DateTime @updatedAt
40}
After updating your schema, run a migration:
1npx prisma migrate dev --name better-auth-migration
π Step 4: Update Your Route Handler
In your app/api/auth directory, rename the [...nextauth] folder to [...all] to avoid confusion. Then, update the route.ts file:
1// app/api/auth/[...all]/route.ts
2import { toNextJsHandler } from "better-auth/next-js";
3import { auth } from "~/server/auth";
4
5export const { POST, GET } = toNextJsHandler(auth);
This handler will process all authentication-related requests, including sign-in, sign-out, and session management.
π§ Step 5: Refactor the Client
Create a reusable auth client file to centralize your authentication hooks and functions:
1// lib/auth-client.ts
2import { createAuthClient } from "better-auth/react";
3
4export const authClient = createAuthClient({
5 baseURL: process.env.NEXT_PUBLIC_BASE_URL // Optional if your API is on the same domain
6});
7
8export const { signIn, signOut, useSession } = authClient;
Example: Social Login with Discord
Update your social login functions to use Better Auth's type-safe API:
1import { signIn } from "~/lib/auth-client";
2
3export const signInDiscord = async () => {
4 const { data, error } = await signIn.social({
5 provider: "discord",
6 callbackUrl: "/dashboard" // Optional redirect after successful login
7 });
8
9 if (error) {
10 console.error("Failed to sign in:", error.message);
11 return null;
12 }
13
14 // Success! User is now logged in
15 return data;
16};
Example: Email and Password Login
1import { signIn } from "~/lib/auth-client";
2
3export const signInWithEmail = async (email: string, password: string) => {
4 const { data, error } = await signIn.email({
5 email,
6 password,
7 callbackUrl: "/dashboard" // Optional redirect after successful login
8 });
9
10 if (error) {
11 return { success: false, message: error.message };
12 }
13
14 return { success: true, data };
15};
Example: Replace useSession
Replace Auth.js's useSession hook with Better Auth's version:
1import { useSession } from "~/lib/auth-client";
2
3export const Profile = () => {
4 const { data, isLoading, error } = useSession();
5
6 if (isLoading) return <div>Loading user information...</div>;
7 if (error) return <div>Error: {error.message}</div>;
8 if (!data) return <div>Please sign in to view your profile</div>;
9
10 return (
11 <div>
12 <h1>Welcome, {data.user.name || 'User'}</h1>
13 <img src={data.user.image || '/default-avatar.png'} alt="Profile" />
14 <pre>{JSON.stringify(data, null, 2)}</pre>
15 </div>
16 );
17};
π§Ύ Step 6: Server Actions and Middleware
Server-Side Session Access
For server components or server actions, use the auth instance to get session data:
1// Server Component
2import { auth } from "~/server/auth";
3import { headers } from "next/headers";
4
5export default async function Dashboard() {
6 const session = await auth.api.getSession({
7 headers: headers(),
8 });
9
10 if (!session) {
11 redirect("/login");
12 }
13
14 return (
15 <div>
16 <h1>Welcome, {session.user.name}</h1>
17 {/* Dashboard content */}
18 </div>
19 );
20}
1// Server Action
2"use server";
3
4import { auth } from "~/server/auth";
5import { headers } from "next/headers";
6import { revalidatePath } from "next/cache";
7
8export const updateUserProfile = async (formData: FormData) => {
9 const session = await auth.api.getSession({
10 headers: headers(),
11 });
12
13 if (!session) {
14 throw new Error("Unauthorized");
15 }
16
17 // Update user profile logic here
18
19 revalidatePath("/profile");
20 return { success: true };
21};
Middleware (Optional)
To protect routes with middleware, create a middleware.ts file in your project root:
1// middleware.ts
2import { auth } from "~/server/auth";
3import { NextResponse } from "next/server";
4import type { NextRequest } from "next/server";
5
6export async function middleware(req: NextRequest) {
7 const session = await auth.api.getSession({
8 headers: req.headers
9 });
10
11 // Redirect unauthenticated users to login page
12 if (!session && !req.nextUrl.pathname.startsWith("/login")) {
13 const loginUrl = new URL("/login", req.url);
14 loginUrl.searchParams.set("callbackUrl", req.nextUrl.pathname);
15 return NextResponse.redirect(loginUrl);
16 }
17
18 // Optional: Role-based access control
19 if (
20 session &&
21 req.nextUrl.pathname.startsWith("/admin") &&
22 session.user.role !== "admin"
23 ) {
24 return NextResponse.redirect(new URL("/dashboard", req.url));
25 }
26
27 return NextResponse.next();
28}
29
30export const config = {
31 matcher: [
32 "/dashboard/:path*",
33 "/profile/:path*",
34 "/admin/:path*"
35 ]
36};
β Final Checklist
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 |
π Wrapping Up
You've successfully migrated from Auth.js to Better Auth! Your application now benefits from:
- Type-safe authentication APIs
- Improved performance and security
- More flexible session management
- Better developer experience
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.
πββοΈ FAQ
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.