How to use Prisma ORM with Better-Auth and Next.js
Introduction
Better-Auth is a modern, open-source authentication solution for web applications. It's built with TypeScript, React, and Prisma to provide a simple and extensible auth experience.
In this guide, you'll wire Better-Auth into a brand-new Next.js app and persist users in a Prisma Postgres database. You can find a complete example of this guide on GitHub.
Prerequisites
- Node.js 18+
- Basic familiarity with Next.js App Router and Prisma
1. Set up your project
Create a new Next.js application:
npx create-next-app@latest betterauth-nextjs-prisma
It will prompt you to customize your setup. Choose the defaults:
- Would you like to use TypeScript?
Yes
- Would you like to use ESLint?
Yes
- Would you like to use Tailwind CSS?
Yes
- Would you like your code inside a
src/
directory?Yes
- Would you like to use App Router?
Yes
- Would you like to use Turbopack?
Yes
- Would you like to customize the import alias (
@/*
by default)?No
Navigate to the project directory:
cd betterauth-nextjs-prisma
These selections will create a modern Next.js project with TypeScript for type safety, ESLint for code quality, and Tailwind CSS for styling. Using the src/
directory and the App Router are common conventions for new Next.js applications.
2. Set up Prisma
Next, you'll add Prisma to your project to manage your database.
2.1. Install Prisma and dependencies
Install the necessary Prisma packages. The dependencies differ slightly depending on whether you use Prisma Postgres with Accelerate or another database.
- Prisma Postgres (recommended)
- Other databases
npm install prisma --save-dev
npm install @prisma/extension-accelerate @prisma/client
npm install prisma --save-dev
npm install @prisma/client
Once installed, initialize Prisma in your project:
npx prisma init --db --output ../src/generated/prisma
You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My Better-Auth Project"
This will create:
- A
prisma
directory with aschema.prisma
file. - A Prisma Postgres database.
- A
.env
file containing theDATABASE_URL
at the project root. - An
output
directory for the generated Prisma Client asbetter-auth/generated/prisma
.
2.2. Configure the Prisma client generator
Run the following command to create the database tables and generate the Prisma Client:
npx prisma generate
2.3. Set up a global Prisma client
Create a /lib
directory and a prisma.ts
file inside it. This file will be used to create and export your Prisma Client instance.
mkdir -p src/lib
touch src/lib/prisma.ts
Set up the Prisma client like this:
- Prisma Postgres (recommended)
- Other databases
import { PrismaClient } from "@/generated/prisma";
import { withAccelerate } from "@prisma/extension-accelerate";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate());
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
import { PrismaClient } from "@/generated/prisma";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
We recommend using a connection pooler (like Prisma Accelerate) to manage database connections efficiently.
If you choose not to use one, avoid instantiating PrismaClient
globally in long-lived environments. Instead, create and dispose of the client per request to prevent exhausting your database connections.
3. Set up Better-Auth
Now it's time to integrate Better-Auth for authentication.
3.1. Install and configure Better-Auth
First, install the Better-Auth core package:
npm install better-auth
Next, generate a secure secret that Better-Auth will use to sign authentication tokens. This ensures your tokens cannot be messed with.
npx @better-auth/cli@latest secret
Copy the generated secret and add it, along with your application's URL, to your .env
file:
# Better-Auth
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=http://localhost:3000
# Prisma
DATABASE_URL="your-database-url"
Now, create a configuration file for Better-Auth at src/lib/auth.ts
:
touch src/lib/auth.ts
In this file, you'll configure Better-Auth to use the Prisma adapter, which allows it to persist user and session data in your database. You will also enable email and password authentication.
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
})
Better-Auth also supports other sign-in methods like social logins (Google, GitHub, etc.), which you can explore in their documentation.
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
})
If your application runs on a port other than 3000
, you must add it to the trustedOrigins
in your auth.ts
configuration to avoid CORS errors during authentication requests.
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
trustedOrigins: ['http://localhost:3001'],
})
3.2. Add Better-Auth models to your schema
Better-Auth provides a CLI command to automatically add the necessary authentication models (User
, Session
, Account
, and Verification
) to your schema.prisma
file.
Run the following command:
npx @better-auth/cli generate
It will ask for confirmation to overwrite your existing Prisma schema. Select y
.
This will add the following models:
model User {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}
3.3. Migrate the database
With the new models in your schema, you need to update your database. Run a migration to create the corresponding tables:
npx prisma migrate dev --name add-auth-models
4. Set up the API routes
Better-Auth needs an API endpoint to handle authentication requests like sign-in, sign-up, and sign-out. You'll create a catch-all API route in Next.js to handle all requests sent to /api/auth/[...all]
.
First, create the necessary directory and file:
mkdir -p "src/app/api/auth/[...all]"
touch "src/app/api/auth/[...all]/route.ts"
Add the following code to the newly created route.ts
file. This code uses a helper from Better-Auth to create Next.js-compatible GET
and POST
request handlers.
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
Next, you'll need a client-side utility to interact with these endpoints from your React components. Create a new file src/lib/auth-client.ts
:
touch src/lib/auth-client.ts
Add the following code, which creates the React hooks and functions you'll use in your UI:
import { createAuthClient } from 'better-auth/react'
export const { signIn, signUp, signOut, useSession } = createAuthClient()
5. Set up your pages
Now, let's build the user interface for authentication. Create the pages for signing up, signing in, and a protected dashboard:
mkdir -p src/app/{sign-up,sign-in,dashboard}
touch src/app/{sign-up,sign-in,dashboard}/page.tsx
5.1. Sign up page
First, create the basic SignUpPage
component in src/app/sign-up/page.tsx
. This sets up the main container and a title for your page.
"use client";
export default function SignUpPage() {
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
Next, import the necessary hooks from React and Next.js to manage state and navigation. Initialize the router and a state variable to hold any potential error messages.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
Now, import the signUp
function from your Better-Auth client and add the handleSubmit
function. This function is triggered on form submission and calls the signUp.email
method provided by Better-Auth, passing the user's name, email, and password.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
//add-next-lin
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
To inform the user of any issues, add an element that conditionally renders when the error
state is not null.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
{error && <p className="text-red-500">{error}</p>}
</main>
);
}
Finally, add the HTML form with input fields for the user's name, email, and password, and a submit button.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="name"
placeholder="Full Name"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
minLength={8}
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Create Account
</button>
</form>
</main>
);
}
5.2. Sign in page
For the sign-in page, start with the basic structure in src/app/sign-in/page.tsx
.
"use client";
export default function SignInPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
Now, add the state and router hooks, similar to the sign-up page.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
Add the handleSubmit
function, this time importing and using the signIn.email
method from Better-Auth.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
Add the conditional error message display.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
{error && <p className="text-red-500">{error}</p>}
</main>
);
}
Finally, add the form fields for email and password and a sign-in button.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign In
</button>
</form>
</main>
);
}
5.3. Dashboard page
This is the protected page for authenticated users. Start with the basic component in src/app/dashboard/page.tsx
.
"use client";
export default function DashboardPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
Import the useSession
hook from your Better-Auth client. This hook is the key to managing authentication state on the client side. It provides the session data and a pending status.
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
To protect this route, use a useEffect
hook. This effect checks if the session has loaded (!isPending
) and if there is no authenticated user (!session?.user
). If both are true, it redirects the user to the sign-in page.
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
//add-start: redirect if not signed in
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
To provide a better user experience, add loading and redirecting states while the session is being verified.
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
//add-start: loading and redirect states
if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
Finally, if the user is authenticated, display their name and email from the session
object. Also, import the signOut
function and add a button that calls it, allowing the user to log out.
"use client";
import { useRouter } from "next/navigation";
import { useSession, signOut } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;
//add-start: destructure user from session
const { user } = session;
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p>Welcome, {user.name || "User"}!</p>
<p>Email: {user.email}</p>
{/* add-start: sign out button */}
<button
onClick={() => signOut()}
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign Out
</button>
</main>
);
}
5.4. Home page
Finally, update the home page to provide simple navigation to the sign-in and sign-up pages. Replace the contents of src/app/page.tsx
with the following:
"use client";
import { useRouter } from "next/navigation";
export default function Home() {
const router = useRouter();
return (
<main className="flex items-center justify-center h-screen bg-neutral-950 text-white">
<div className="flex gap-4">
<button
onClick={() => router.push("/sign-up")}
className="bg-white text-black font-medium px-6 py-2 rounded-md hover:bg-gray-200">
Sign Up
</button>
<button
onClick={() => router.push("/sign-in")}
className="border border-white text-white font-medium px-6 py-2 rounded-md hover:bg-neutral-800">
Sign In
</button>
</div>
</main>
);
}
6. Test it out
Your application is now fully configured.
- Start the development server to test it:
npm run dev
-
Navigate to
http://localhost:3000
in your browser. You should see the home page with "Sign Up" and "Sign In" buttons. -
Click on Sign Up, create a new account, and you should be redirected to the dashboard. You can then sign out and sign back in.
-
To view the user data directly in your database, you can use Prisma Studio.
npx prisma studio
- This will open a new tab in your browser where you can see the
User
,Session
, andAccount
tables and their contents.
Congratulations! You now have a fully functional authentication system built with Better-Auth, Prisma, and Next.js.
Next steps
- Add support for social login or magic links
- Implement password reset and email verification
- Add user profile and account management pages
- Deploy to Vercel and secure your environment variables
- Extend your Prisma schema with custom application models
Further reading
Stay connected with Prisma
Continue your Prisma journey by connecting with our active community. Stay informed, get involved, and collaborate with other developers:
- Follow us on X for announcements, live events and useful tips.
- Join our Discord to ask questions, talk to the community, and get active support through conversations.
- Subscribe on YouTube for tutorials, demos, and streams.
- Engage on GitHub by starring the repository, reporting issues, or contributing to an issue.