In this post I’ll show how I integrated Better-Auth into a Next.js App Router project that uses:
- Prisma only for schema management
- Kysely as the type-safe query builder
- Any SQL database (I am using Neon Postgres, but you can use others)
📌 Before you continue:
This guide builds on top of the Prisma + Kysely setup from my previous blog.
If you haven’t done that setup yet, this guide will not work as-is. please read it first -
👉 Using Prisma for Schema and Kysely for Queries in a Next.js App
1. Install Better-Auth Packages
I prefer pnpm, so I’ll use it. You can replace it with npm/yarn.
Install only the packages we need:
pnpm add better-auth better-auth/next-js better-auth/react
This is the full Better-Auth stack for Next.js App Router + React hooks.
2. (Optional) Database Setup with Kysely + Prisma
My setup uses:
- Prisma → schema
- prisma-kysely → generate DB types
- Kysely → actual querying
The full database setup (scripts, configs, generators, etc.) is explained in my previous blog.
Here, we only focus on the schema required by Better-Auth.
3. Schema Required by Better-Auth
Better-Auth depends on the following models:
UserSessionAccountVerification
Paste this exact schema in your prisma/schema.prisma:
model User {
id String @id @db.Text
email String @unique @db.Text
name String @db.Text
image String? @default("") @db.Text
emailVerified Boolean @default(false)
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
sessions Session[]
accounts Account[]
@@map("user")
}
model Session {
id String @id @db.Text
expiresAt DateTime @db.Timestamptz
token String @unique @db.Text
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
ipAddress String? @db.Text
userAgent String? @db.Text
userId String @db.Text
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("session")
}
model Account {
id String @id @db.Text
accountId String @db.Text
providerId String @db.Text
userId String @db.Text
accessToken String? @db.Text
refreshToken String? @db.Text
idToken String? @db.Text
accessTokenExpiresAt DateTime? @db.Timestamptz
refreshTokenExpiresAt DateTime? @db.Timestamptz
scope String? @db.Text
password String? @db.Text
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("account")
}
model Verification {
id String @id @db.Text
identifier String @db.Text
value String @db.Text
expiresAt DateTime @db.Timestamptz
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz
@@map("verification")
}
Now generate types and push schema:
pnpm db:generate
pnpm db:push
-
db:generate→ Converts Prisma models into Kysely types -
db:push→ Updates your actual database
These generated types are required for Kysely to be fully type-safe.
4. Kysely Client
Below is the minimal Kysely client used in this blog.
I am using Neon, but you can use any provider by switching the dialect:
// server/db/index.ts
import { Kysely } from 'kysely'
import { NeonDialect } from 'kysely-neon'
import { neon } from '@neondatabase/serverless'
import type { DB } from '@/db/types/kysely'
export const db = new Kysely<DB>({
dialect: new NeonDialect({
neon: neon(process.env.DATABASE_URL!),
}),
})
5. Environment Variables (.env)
Create/update your .env:
DATABASE_URL="postgresql://user:password@host:5432/dbname?sslmode=require"
BETTER_AUTH_SECRET="paste-generated-secret-here"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
Generate BETTER_AUTH_SECRET
openssl rand -base64 32
Paste it into .env.
6. Better-Auth Setup
6.1. Server Auth (lib/auth.ts)
// lib/auth.ts
import { betterAuth } from 'better-auth'
import { db } from '@/server/db'
const secret = process.env.BETTER_AUTH_SECRET
if (!secret) throw new Error('BETTER_AUTH_SECRET is not configured')
export const auth = betterAuth({
secret,
database: {
db,
type: 'postgres',
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
})
export type Session = typeof auth.$Infer.Session
6.2. Next.js API Route
// app/api/auth/[...all]/route.ts
import { toNextJsHandler } from 'better-auth/next-js'
import { auth } from '@/lib/auth'
export const { GET, POST } = toNextJsHandler(auth)
6.3. Client Helpers
// lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({
basePath: '/api/auth',
})
export const { signIn, signOut, signUp, useSession } = authClient
7. Implementing the Auth Flow
Below are simple examples that help you understand how to actually use Better-Auth in a basic Next.js app.
7.1. Sign-In Form (Google + GitHub)
// components/login-form.tsx
'use client'
import { useState, useTransition } from 'react'
import { signIn } from '@/lib/auth-client'
export function LoginForm() {
const [pending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const handleSignIn = (provider: 'google' | 'github') => {
startTransition(async () => {
const { error } = await signIn.social({
provider,
callbackURL: '/app',
})
if (error) setError(error.message)
})
}
return (
<div className="space-y-4">
<button disabled={pending} onClick={() => handleSignIn('google')} className="w-full rounded border px-3 py-2">
Continue with Google
</button>
<button disabled={pending} onClick={() => handleSignIn('github')} className="w-full rounded border px-3 py-2">
Continue with GitHub
</button>
{error && <p className="text-sm text-red-500">{error}</p>}
</div>
)
}
7.2. Simple Sign-Out Example
// components/signout-button.tsx
'use client'
import { useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { signOut } from '@/lib/auth-client'
export function SignOutButton() {
const [pending, startTransition] = useTransition()
const router = useRouter()
const handleSignOut = () => {
startTransition(async () => {
await signOut({
fetchOptions: {
onSuccess: () => router.push('/signin'),
},
})
})
}
return (
<button onClick={handleSignOut} disabled={pending} className="rounded border px-3 py-1">
{pending ? 'Signing out...' : 'Sign out'}
</button>
)
}
7.3. Protecting a Server Component Route
// app/(app)/layout.tsx
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
import { SignOutButton } from '@/components/signout-button'
export default async function AppLayout({ children }) {
const session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect('/signin')
return (
<div className="p-4 space-y-6">
<SignOutButton />
{children}
</div>
)
}
7.4. Server Action with Auth Guard
// server/actions/get-user-data.ts
'use server'
import { headers } from 'next/headers'
import { auth } from '@/lib/auth'
export async function getUserData() {
const session = await auth.api.getSession({ headers: await headers() })
if (!session) throw new Error('Unauthorized')
// You Server codes/ DB fetches
// return your response
}
7.5. Securing an API Route
// app/api/profile/route.ts
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
export async function GET() {
const session = await auth.api.getSession({ headers: await headers() })
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
return NextResponse.json({
email: session.user.email,
})
}
8. Extensions
Better-Auth supports:
- Password-based auth
- Passkeys
- Email login
- Multi-factor authentication
- Webhooks
- More providers
All extensions plug into the same config shown earlier.
See the official documentation for details.
9. Folder Structure Used in This Blog
Here’s how the complete Better-Auth + Kysely + Prisma setup looks in a clean and simple structure:
📦 my-next-app
├── 📁 app
│ ├── 📁 api
│ │ └── 📁 auth
│ │ └── [...all]
│ │ └── route.ts
│ ├── 📁 (auth)
│ │ └── 📁 signin
│ │ └── page.tsx
│ └── 📁 (app)
│ ├── layout.tsx
│ └── 📁 app
│ └── page.tsx
│
├── 📁 components
│ ├── login-form.tsx
│ └── signout-button.tsx
│
├── 📁 db
│ ├── schema.prisma
│ └── 📁 types
│ └── kysely.d.ts
│
├── 📁 lib
│ ├── auth.ts
│ └── auth-client.ts
│
├── 📁 server
│ ├── 📁 db
│ │ └── index.ts
│ └── 📁 actions
│ └── get-user-email.ts
│
├── .env
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
Top comments (1)
very helpful article 👌