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)

πŸ“Œ Befo…


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:

  • 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")
}

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


Print Share Comment Cite Upload Translate Updates
APA

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/

MLA
" » Setting Up Better-Auth in Next.js with Kysely + Prisma Schema." Golam Rabbani | Sciencx - Friday November 14, 2025, https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/
HARVARD
Golam Rabbani | Sciencx Friday November 14, 2025 » Setting Up Better-Auth in Next.js with Kysely + Prisma Schema., viewed ,<https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/>
VANCOUVER
Golam Rabbani | Sciencx - » Setting Up Better-Auth in Next.js with Kysely + Prisma Schema. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/
CHICAGO
" » Setting Up Better-Auth in Next.js with Kysely + Prisma Schema." Golam Rabbani | Sciencx - Accessed . https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/
IEEE
" » Setting Up Better-Auth in Next.js with Kysely + Prisma Schema." Golam Rabbani | Sciencx [Online]. Available: https://www.scien.cx/2025/11/14/setting-up-better-auth-in-next-js-with-kysely-prisma-schema/. [Accessed: ]
rf:citation
» Setting Up Better-Auth in Next.js with Kysely + Prisma Schema | Golam Rabbani | Sciencx | 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.

You must be logged in to translate posts. Please log in or register.