Authentication is one of those things every SaaS product needs but nobody wants to build from scratch. The options have historically been: use a hosted service (Auth0, Clerk) and pay per user, or roll your own and spend a week on password reset flows.
Better Auth is a third option: an open-source auth library that runs in your own infrastructure, handles all the common flows, and does not charge per monthly active user.
What Better Auth gives you out of the box
- Email and password authentication with proper hashing (argon2)
- Password reset via email
- Email verification (optional)
- OAuth providers (Google, GitHub, and others)
- Session management with refresh tokens
- TypeScript types throughout
It stores everything in your database using your ORM. With Drizzle and Cloudflare D1, sessions live in your D1 database and the full auth flow runs at the Cloudflare edge.
Basic setup
Install the package:
npm install better-auth
Create the auth configuration. This is where you define your database adapter, session settings, and providers:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/server/db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24, // refresh if older than 1 day
},
});
Create the catch-all API route that handles all auth requests:
// src/app/api/auth/[...all]/route.ts
import { auth } from "@/server/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
The client setup
Better Auth provides a typed client that matches your server configuration:
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});
Use it in your components:
const { data: session } = authClient.useSession();
const { signIn, signOut } = authClient;
Google OAuth in five minutes
- Go to console.cloud.google.com
- Create a new project (or use an existing one)
- Enable the Google OAuth2 API
- Create OAuth 2.0 credentials (Web application type)
- Add your callback URL:
https://yourdomain.com/api/auth/callback/google - Copy the Client ID and Client Secret into your environment variables
For local development, add http://localhost:3000/api/auth/callback/google as an authorized redirect URI.
Password reset flow
Better Auth handles the full password reset flow. You just need to configure the email sender:
export const auth = betterAuth({
// ...
emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
react: PasswordResetEmail({ resetUrl: url }),
});
},
},
});
The URL is a signed, time-limited link that Better Auth generates and validates automatically.
Getting the current user in a Server Component
import { auth } from "@/server/auth";
import { headers } from "next/headers";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) redirect("/login");
return <div>Hello, {session.user.name}</div>;
}
This is the pattern used throughout Flint — server-side session checks with automatic redirect for unauthenticated users.