Better Auth
Email/password and Google OAuth authentication with route protection.
Overview
Adds Better Auth with email/password login, Google OAuth, route protection, and server-side session validation. Sessions are stored in the database via the Prisma or Drizzle adapter.
Prerequisites
Installation
npx ds-start init my-app --prisma --auth
# or
npx ds-start init my-app --drizzle --authWhat It Adds
lib/auth.ts— Better Auth server configurationlib/auth-client.ts— Client-side auth helpersapp/api/auth/[...all]/route.ts— Auth API route handlerapp/(auth)/sign-in/page.tsx— Sign-in page with formapp/(auth)/sign-up/page.tsx— Sign-up page with formapp/dashboard/page.tsx— Protected dashboard page
Environment Variables
| Variable | Description |
|---|---|
BETTER_AUTH_SECRET | Auth encryption secret |
BETTER_AUTH_URL | Application URL for callbacks |
GOOGLE_CLIENT_ID | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
Usage
Server configuration
import { betterAuth } from "better-auth"
import { prismaAdapter } from "better-auth/adapters/prisma"
import { prisma } from "@/lib/prisma"
export const auth = betterAuth({
appName: "Dev-Start",
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) => {
console.log(`[auth] Password reset for ${user.email}: ${url}`)
},
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
enabled: Boolean(process.env.GOOGLE_CLIENT_ID),
},
},
})Client setup
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient()API route handler
import { toNextJsHandler } from "better-auth/next-js"
import { auth } from "@/lib/auth"
export const { GET, POST } = toNextJsHandler(auth)Sign-in form
"use client"
import { authClient } from "@/lib/auth-client"
import { useRouter } from "next/navigation"
import { useState, useTransition } from "react"
export function SignInForm() {
const router = useRouter()
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isPending, startTransition] = useTransition()
function handleSubmit(formData: FormData) {
const email = formData.get("email")?.toString().trim() ?? ""
const password = formData.get("password")?.toString() ?? ""
startTransition(async () => {
setErrorMessage(null)
const result = await authClient.signIn.email({ email, password })
if (result.error) {
setErrorMessage(result.error.message ?? "Authentication failed.")
return
}
router.push("/dashboard")
router.refresh()
})
}
function handleGoogleSignIn() {
startTransition(async () => {
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
})
})
}
return (
<form action={handleSubmit}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
{errorMessage && <p>{errorMessage}</p>}
<button type="submit" disabled={isPending}>Sign in</button>
<button type="button" onClick={handleGoogleSignIn} disabled={isPending}>
Sign in with Google
</button>
</form>
)
}Protecting a page (server-side)
import { headers } from "next/headers"
import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
})
if (!session) {
redirect("/sign-in")
}
return (
<main>
<p>Name: {session.user.name}</p>
<p>Email: {session.user.email}</p>
<p>Expires: {new Date(session.session.expiresAt).toLocaleString()}</p>
</main>
)
}