Estimated time: 25 minutes
Better Auth is a modern, open-source authentication solution for web applications. It is built with TypeScript and provides a simple and extensible authentication experience with support for multiple database adapters, including Prisma.
In this guide, you will wire Better Auth into a brand-new Next.js application and persist users in a Prisma Postgres database.
- Node.js 20+
- Basic familiarity with Next.js App Router
- Basic familiarity with Prisma
Create a new Next.js application:
npx create-next-app@latest betterauth-nextjs-prismaChoose the default options:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- Use
src/directory: Yes - App Router: Yes
- Turbopack: Yes
- Customize import alias: No
Navigate into the project directory:
cd betterauth-nextjs-prismaThis creates a modern Next.js project with TypeScript, ESLint, Tailwind CSS, and App Router.
npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pgInitialize Prisma:
npx prisma init --db --output ../src/generated/prismaThis creates:
prisma/schema.prisma- A Prisma Postgres database
.envwithDATABASE_URL- Generated Prisma Client output directory
Create prisma.config.ts at the project root:
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});npx prisma generateCreate the Prisma client utility:
mkdir -p src/lib
touch src/lib/prisma.tsimport { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma || new PrismaClient({ adapter });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;npm install better-authGenerate a secure secret:
npx @better-auth/cli@latest secretUpdate .env:
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL="your-database-url"Create src/lib/auth.ts:
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 using a different port, configure trusted origins:
trustedOrigins: ['http://localhost:3001']npx @better-auth/cli generateThis adds User, Session, Account, and Verification models to schema.prisma.
npx prisma migrate dev --name add-auth-models
npx prisma generateCreate the API route:
mkdir -p src/app/api/auth/[...all]
touch src/app/api/auth/[...all]/route.tsimport { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);Create the client helper:
touch src/lib/auth-client.tsimport { createAuthClient } from 'better-auth/react'
export const { signIn, signUp, signOut, useSession } = createAuthClient()mkdir -p src/app/{sign-up,sign-in,dashboard}
touch src/app/{sign-up,sign-in,dashboard}/page.tsxComplete src/app/sign-up/page.tsx:
"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 />
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" minLength={8} required />
<button type="submit">Create Account</button>
</form>
</main>
);
}"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 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" required />
<input name="password" type="password" required />
<button type="submit">Sign In</button>
</form>
</main>
);
}"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useSession, signOut } from "@/lib/auth-client";
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>Loading...</p>;
if (!session?.user) return <p>Redirecting...</p>;
const { user } = session;
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p>Welcome, {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</main>
);
}"use client";
import { useRouter } from "next/navigation";
export default function Home() {
const router = useRouter();
return (
<main className="flex items-center justify-center h-screen">
<button onClick={() => router.push("/sign-up")}>Sign Up</button>
<button onClick={() => router.push("/sign-in")}>Sign In</button>
</main>
);
}Start the development server:
npm run devVisit http://localhost:3000.
- Sign up with a new account
- Access the dashboard
- Sign out and sign back in
Open Prisma Studio to inspect the database:
npx prisma studioYou now have a fully functional authentication system using Better Auth, Prisma, and Next.js, with email and password authentication backed by a Postgres database.