DEV Community

Cover image for Setting Up Better-Auth in Next.js with Kysely + Prisma Schema
Golam Rabbani
Golam Rabbani

Posted on

Setting Up Better-Auth in Next.js with Kysely + Prisma Schema

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
Enter fullscreen mode Exit fullscreen mode

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:

  • User
  • Session
  • Account
  • Verification

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")
}
Enter fullscreen mode Exit fullscreen mode

Now generate types and push schema:

pnpm db:generate
pnpm db:push
Enter fullscreen mode Exit fullscreen mode
  • 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!),
  }),
})
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Generate BETTER_AUTH_SECRET

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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,
  })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
sobitp59 profile image
Sobit Prasad

very helpful article 👌