This content originally appeared on DEV Community and was authored by Golam Rabbani
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
This content originally appeared on DEV Community and was authored by Golam Rabbani
Golam Rabbani | Sciencx (2025-11-14T12:12:40+00:00) Setting Up Better-Auth in Next.js with Kysely + Prisma Schema. Retrieved from https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.