BetterAuth Setup in Next.js for SaaS Apps
Authentication is the foundation of any SaaS product. It needs to be secure, scalable, and developer-friendly while supporting modern flows like social logins, magic links, and role-based access control (RBAC).
This guide walks you through setting up a production-ready authentication system in Next.js 16+ using BetterAuth a powerful, type-safe auth library built specifically for modern Next.js apps.
SaaSJet uses this exact setup out of the box, so you get email/password, social providers (Google, GitHub), and protected routes from day one.
Why BetterAuth is Perfect for SaaS Builders
- Full-stack TypeScript – End-to-end type safety with Prisma
- Flexible providers – Email/password, social logins and more!
- Built-in RBAC – Roles, organizations, team invites
- Secure by default – Rate limiting, email verification, secure cookies
- No lock-in – Open source, self-hostable, easy to customize
Step 1: Install BetterAuth
npm install better-auth @better-auth/prisma
Also install Prisma if you haven't:
npm install prisma @prisma/client
npx prisma init
Step 2: Set Up Your Database Schema
Update your prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // or "sqlite" for dev
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
emailVerified Boolean @default(false)
name String?
image String?
role String @default("user") // admin, user, etc.
createdAt DateTime @default(now())
sessions Session[]
accounts Account[]
}
model Session {
id String @id
userId String
expiresAt DateTime
user User @relation(fields: [userId], references: [id])
}
model Account {
id String @id @default(cuid())
userId String
provider String
providerAccountId String
user User @relation(fields: [userId], references: [id])
@@unique([provider, providerAccountId])
}
Run migration:
npx prisma migrate dev --name init
Step 3: Initialize BetterAuth
Create src/lib/auth.ts:
import { betterAuth } from "better-auth";
import prisma from "@/lib/prisma";
import { prismaAdapter } from "better-auth/adapters/prisma";
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET!,
database: prismaAdapter(prisma, {
provider: "postgresql"
}),
callbacks: {
session: async ({ session, user }: any) => {
// attach custom fields to session.user
session.user.stripeCustomerId = user.stripeCustomerId;
return session;
},
},
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}
},
})
Add your provider credentials to .env.local:
GOOGLE_CLIENT_ID=your_google_id
GOOGLE_CLIENT_SECRET=your_google_secret
GITHUB_CLIENT_ID=your_github_id
GITHUB_CLIENT_SECRET=your_github_secret
BETTER_AUTH_SECRET=your_strong_random_secret
BETTER_AUTH_URL=http://localhost:3000
Step 4: Create API Routes
BetterAuth automatically generates all routes. Just import the handler:
// src/app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
That’s it — all auth endpoints are now live at /api/auth/*.
Step 5: Protect Routes (Middleware + Server Components)
Middleware ProtectionCreate middleware.ts:
import { auth } from "@/lib/auth";
export default auth.middleware;
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};
Server-Side Check in Components
import { auth } from "@/lib/auth";
export default async function Dashboard() {
const session = await auth.api.getSession();
if (!session.user) {
redirect("/sign-in");
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Role: {session.user.role}</p>
</div>
);
}
Role-Based Access Control
if (session.user.role !== "admin") {
notFound(); // or redirect
}
Step 6: Sign-In Page (Client Component)
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { signIn } from "@/lib/auth-client";
export function SignInForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleSignIn = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const res = await signIn.email({
email,
password,
rememberMe: true,
callbackURL: "/dashboard",
});
if (res?.data) {
console.log("Successfully signed in!", res);
} else {
console.log("Sign-in failed:", res?.error);
}
setLoading(false);
};
return (
<form onSubmit={handleSignIn} className="space-y-5">
<Input
placeholder="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="rounded-xl border-neutral-500"
/>
<Input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="rounded-xl border-neutral-500"
/>
<Button type="submit" variant={"secondary"} className="w-full rounded-xl cursor-pointer" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
</Button>
</form>
);
}
Final Thoughts
With BetterAuth, you get a complete, secure, and extensible authentication system in minutes no need to build sessions, verification flows, or OAuth from scratch.
SaaSJet ships with all of this fully configured: sign-in pages, protected routes, admin guards, and user management dashboard so you can focus on building your product, not auth plumbing.
Build this faster with SaaSJet → clone the repo saasjet
Happy coding! Published: December 27, 2025