Next.js ile Better Auth: NextAuth'a Gerek Yok — Modern Kimlik Doğrulama Rehberi
Better Auth v1.5 ile Next.js 16'da email/şifre, magic link ve OAuth kurulumu. Drizzle adapter, proxy.ts entegrasyonu, rol tabanlı erişim ve 2FA plugin — sıfırdan production'a tam rehber.

Önemli Not: Bu yazıdaki teknik bilgiler yazım tarihi itibarıyla geçerlidir. Kullanılan kütüphaneler, API'ler ve servisler zaman içinde değişebilir. Ücretlendirme, yasal düzenleme ve vergi konularında ilgili resmi kaynakları ve uzmanları referans alınız. Bu içerik bilgilendirme amaçlı olup herhangi bir finansal veya hukuki tavsiye niteliği taşımamaktadır.
Next.js ekosisteminde kimlik doğrulama söz konusu olduğunda ilk akla gelen isim hâlâ NextAuth — artık Auth.js adıyla bilinen bu kütüphane, uzun yıllar boyunca fiilî standart olarak kabul gördü. Ancak 2025 sonunda bu resim ciddi ölçüde değişti. Better Auth, TypeScript-first tasarımı, zengin plugin ekosistemi ve ücretsiz self-hosted yapısıyla modern Next.js projelerinin yeni kimlik doğrulama tercihi hâline geldi.
Bu rehberde Better Auth v1.5 (Mart 2026) ile Next.js 16 App Router'da sıfırdan kimlik doğrulama sistemi kuruyoruz. Email/şifre girişi, magic link, OAuth provider'ları, Drizzle ORM adapter, proxy.ts ile route koruması ve rol tabanlı erişim — hepsini adım adım ele alıyoruz.
Bu projenin (ilkkod.com) hem admin paneli hem müşteri portalı Better Auth üzerinde çalışıyor. Yani teorik değil, gerçek production deneyimiyle yazılmış bir rehber.
2026'da Next.js Auth Ekosistemi
Bir kütüphane seçmeden önce seçenekleri karşılaştırmak gerekiyor:
| Better Auth | NextAuth v5 (Auth.js) | Clerk | Lucia | |
|---|---|---|---|---|
| Fiyat | Ücretsiz, self-hosted | Ücretsiz, self-hosted | Ücretsiz tier (sınırlı kullanıcı) | Ücretsiz, self-hosted |
| TypeScript | ✅ Tam, type-safe | ✅ İyi | ✅ | ✅ |
| Drizzle adapter | ✅ Resmi | ❌ Yok (Prisma odaklı) | N/A | Manuel |
| Plugin sayısı | 50+ | Sınırlı | Orta | Minimal |
| Vendor lock-in | ❌ | ❌ | ✅ Var | ❌ |
| Durum | ✅ Aktif (v1.5.5) | ✅ Aktif | ✅ Aktif | ⚠️ Bakım modu |
Lucia Mart 2025'te bakım moduna alındı — yeni projelerde önerilmiyor. Clerk, kullanıcı başına ücretlendirmesi ve vendor lock-in riski nedeniyle kendi veri tabanını kontrol etmek isteyen projeler için dezavantajlı. NextAuth v5, hâlâ iyi bir seçenek; ancak Drizzle adapter eksikliği ve database session yönetimi karmaşıklığı, Drizzle kullanan projelerde süreci zorlaştırıyor.
Better Auth ise TypeScript-first mimarisi, resmi Drizzle adapter'ı ve zengin plugin sistemiyle bu tabloda öne çıkıyor.
Kurulum
bun add better-auth
Better Auth, Drizzle ORM ile doğrudan çalışır. Drizzle'ı henüz kurmadıysanız Drizzle ORM rehberimize göz atın.
Veritabanı Şeması
Better Auth, gerekli tabloları Drizzle şemanıza otomatik ekleyebilir. Bunun için CLI aracını kullanın:
bunx better-auth@latest generate
Bu komut, seçtiğiniz plugin'lere göre user, session, account, verification tablolarının Drizzle şema kodunu çıkarır. Çıktıyı mevcut şema dosyanıza ekleyin ya da ayrı bir dosyada tutun:
// lib/db/schema/auth.ts
import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core"
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified")
.$defaultFn(() => false)
.notNull(),
image: text("image"),
createdAt: timestamp("created_at")
.$defaultFn(() => new Date())
.notNull(),
updatedAt: timestamp("updated_at")
.$defaultFn(() => new Date())
.notNull(),
// Rol tabanlı erişim için
role: text("role").$type<"admin" | "user">().default("user").notNull(),
})
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
})
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
})
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").$defaultFn(() => new Date()),
updatedAt: timestamp("updated_at").$defaultFn(() => new Date()),
})
Şemayı veritabanına yansıtın:
bunx drizzle-kit push
auth.ts Konfigürasyon Dosyası
Tüm Better Auth konfigürasyonu tek bir dosyada toplanır:
// lib/auth.ts
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { magicLink } from "better-auth/plugins"
import { db } from "@/lib/db"
import * as schema from "@/lib/db/schema/auth"
import { resend } from "@/lib/email/resend" // Resend örneği
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: schema.user,
session: schema.session,
account: schema.account,
verification: schema.verification,
},
}),
// Email/şifre kimlik doğrulama
emailAndPassword: {
enabled: true,
requireEmailVerification: false, // Admin paneli için false tutabilirsiniz
},
// Magic link plugin (müşteri portalı için)
plugins: [
magicLink({
sendMagicLink: async ({ email, token, url }) => {
await resend.emails.send({
from: "ilkkod <noreply@ilkkod.com>",
to: email,
subject: "Giriş Bağlantınız",
html: `
<p>Müşteri portalına giriş yapmak için aşağıdaki bağlantıya tıklayın:</p>
<a href="${url}" style="
display: inline-block;
padding: 12px 24px;
background: #FFB800;
color: #000;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
">Giriş Yap</a>
<p>Bu bağlantı 5 dakika geçerlidir.</p>
`,
})
},
expiresIn: 300, // 5 dakika (saniye)
allowedAttempts: 1,
disableSignUp: false, // İlk girişte otomatik kayıt
}),
],
// OAuth provider'ları (opsiyonel)
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!,
},
},
// Session konfigürasyonu
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 gün (saniye)
updateAge: 60 * 60 * 24, // Her 24 saatte bir yenile
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 dakika cookie cache
},
},
// Güvenilir kaynaklara izin ver
trustedOrigins: [
process.env.BETTER_AUTH_URL!,
],
})
export type Session = typeof auth.$Infer.Session
export type User = typeof auth.$Infer.Session.user
Zorunlu Ortam Değişkenleri
# .env.local
BETTER_AUTH_SECRET=en-az-32-karakter-uzun-rastgele-bir-deger
BETTER_AUTH_URL=http://localhost:3000 # Production'da gerçek URL
# OAuth (opsiyonel)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
BETTER_AUTH_SECRET minimum 32 karakter olmalıdır. Güvenli bir değer üretmek için:
openssl rand -base64 32
Next.js Route Handler Kurulumu
Better Auth, tüm kimlik doğrulama isteklerini tek bir catch-all Route Handler üzerinden yönetir:
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth"
import { toNextJsHandler } from "better-auth/next-js"
export const { POST, GET } = toNextJsHandler(auth)
Bu kadar. Bu tek dosya /api/auth/sign-in, /api/auth/sign-out, /api/auth/magic-link, /api/auth/callback/* gibi tüm endpoint'leri otomatik olarak oluşturur.
Client Tarafı: Auth Client Kurulumu
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react"
import { magicLinkClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [magicLinkClient()],
})
// Kullanışlı export'lar
export const {
signIn,
signOut,
signUp,
useSession,
} = authClient
Kimlik Doğrulama Yöntemleri
1. Email/Şifre (Admin Paneli)
// components/admin/login-form.tsx
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { authClient } from "@/lib/auth-client"
export function AdminLoginForm() {
const router = useRouter()
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError(null)
const formData = new FormData(e.currentTarget)
const { data, error } = await authClient.signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
})
if (error) {
setError("E-posta veya şifre hatalı.")
setLoading(false)
return
}
router.push("/yonetim-paneli")
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required placeholder="E-posta" />
<input name="password" type="password" required placeholder="Şifre" />
{error && <p>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? "Giriş yapılıyor..." : "Giriş Yap"}
</button>
</form>
)
}
2. Magic Link (Müşteri Portalı)
Magic link, şifresiz giriş için idealdir. Müşteri e-posta adresini girer, gelen magic link'e tıklar ve otomatik olarak giriş yapar. Bu projenin müşteri portalı bu yöntemi kullanıyor.
// components/portal/magic-link-form.tsx
"use client"
import { useState } from "react"
import { authClient } from "@/lib/auth-client"
export function MagicLinkForm() {
const [sent, setSent] = useState(false)
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
const formData = new FormData(e.currentTarget)
const { error } = await authClient.signIn.magicLink({
email: formData.get("email") as string,
callbackURL: "/musteri-portali",
})
if (!error) setSent(true)
setLoading(false)
}
if (sent) {
return (
<p>
E-postanızı kontrol edin. Giriş bağlantısı birkaç saniye içinde
ulaşacak.
</p>
)
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required placeholder="E-posta adresiniz" />
<button type="submit" disabled={loading}>
{loading ? "Gönderiliyor..." : "Giriş Bağlantısı Gönder"}
</button>
</form>
)
}
Magic link callback rotası için ayrıca bir page.tsx oluşturmanıza gerek yok — Better Auth bunu /api/auth handler üzerinden otomatik yönetir.
3. OAuth ile Giriş (Google, GitHub)
// OAuth butonu
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
})
Server Component'larda Session Okuma
Server Component veya Route Handler içinde mevcut kullanıcıyı şu şekilde alırsınız:
// app/(admin)/yonetim-paneli/page.tsx
import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import { redirect } from "next/navigation"
export default async function AdminPage() {
const session = await auth.api.getSession({
headers: await headers(),
})
if (!session) {
redirect("/giris")
}
// session.user.id, session.user.email, session.user.role erişilebilir
return <div>Hoş geldiniz, {session.user.name}</div>
}
Next.js 16 notu: headers() çağrısı artık async — await headers() yazmalısınız. Senkron erişim Next.js 16'da tamamen kaldırıldı.
Client Component'larda Session Okuma
// components/portal/user-menu.tsx
"use client"
import { useSession } from "@/lib/auth-client"
export function UserMenu() {
const { data: session, isPending } = useSession()
if (isPending) return <div>Yükleniyor...</div>
if (!session) return null
return (
<div>
<span>{session.user.email}</span>
</div>
)
}
Next.js 16: proxy.ts ile Route Koruması
Next.js 16'nın en kritik değişikliğinden biri: middleware.ts yerini proxy.ts'e bıraktı. Better Auth'ın resmi belgelerinde proxy.ts için özel bir kısım bulunmasa da entegrasyon oldukça düzgün çalışıyor:
// proxy.ts (proje kökünde — middleware.ts değil!)
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
// Korunan yollar
const ADMIN_PATHS = ["/yonetim-paneli"]
const PORTAL_PATHS = ["/musteri-portali"]
const PUBLIC_PATHS = ["/giris", "/musteri-portali/giris", "/api/auth"]
export async function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Public path'lere dokunma
if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
return NextResponse.next()
}
// Admin panel koruması
if (ADMIN_PATHS.some((p) => pathname.startsWith(p))) {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.redirect(new URL("/giris", request.url))
}
// Sadece admin rolüne izin ver
if (session.user.role !== "admin") {
return NextResponse.redirect(new URL("/", request.url))
}
}
// Müşteri portalı koruması
if (PORTAL_PATHS.some((p) => pathname.startsWith(p))) {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
return NextResponse.redirect(
new URL("/musteri-portali/giris", request.url),
)
}
}
return NextResponse.next()
}
Önemli: proxy.ts'deki fonksiyon adı middleware değil, proxy olmalıdır. Bu Next.js 16'nın zorunlu kıldığı bir değişikliktir.
Rol Tabanlı Erişim
Better Auth'da kullanıcı rolleri için birkaç yaklaşım var. Bu projede en basit yol: user tablosuna role kolonu eklemek.
Şema değişikliği zaten yukarıda gösterildi. Giriş sırasında rol bilgisi session'a otomatik taşınır.
Server Action ile Rol Kontrolü
// lib/auth/require-admin.ts
import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import { redirect } from "next/navigation"
export async function requireAdmin() {
const session = await auth.api.getSession({
headers: await headers(),
})
if (!session || session.user.role !== "admin") {
redirect("/giris")
}
return session
}
Kullanımı:
// app/(admin)/yonetim-paneli/kullanicilar/page.tsx
import { requireAdmin } from "@/lib/auth/require-admin"
export default async function UsersPage() {
const session = await requireAdmin() // Yetkisizse redirect atar
return <div>Kullanıcı yönetimi</div>
}
Better Auth Plugin Sistemi
Better Auth'un 50'yi aşkın plugin'i, kimlik doğrulamayı ihtiyaç duyduğunuz kadar genişletmenizi sağlar.
İki Faktörlü Doğrulama (2FA)
// lib/auth.ts — plugin ekle
import { twoFactor } from "better-auth/plugins"
export const auth = betterAuth({
// ... diğer konfigürasyon
plugins: [
magicLink({ /* ... */ }),
twoFactor({
issuer: "ilkkod",
otpOptions: {
digits: 6,
period: 30,
},
}),
],
})
Client tarafında:
import { twoFactorClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
magicLinkClient(),
twoFactorClient({
twoFactorPage: "/2fa",
}),
],
})
// 2FA etkinleştirme (TOTP QR kodu üretir)
const { data } = await authClient.twoFactor.enable({
password: "mevcut-sifre",
})
// data.totpURI → QR kodu için kullanın
// Giriş sırasında 2FA kodu doğrulama
await authClient.twoFactor.verifyTotp({ code: "123456" })
Diğer Önemli Plugin'ler
- Email OTP: Magic link yerine 6 haneli kod gönderme
- Passkey: WebAuthn / parmak izi / yüz tanıma desteği
- Organization: Çok kiracılı (multi-tenant) yapılar için takım yönetimi
- Admin: Kullanıcı listeleme, yasaklama, impersonation
- Rate Limiting: Kaba kuvvet saldırılarına karşı built-in koruma
Session Süresi ve Token Yönetimi
Better Auth, cookie tabanlı session yönetimi kullanır. JWT değil, veritabanı session'ı — bu daha güvenli, ancak her istekte DB sorgusu gerektirir.
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 gün
updateAge: 60 * 60 * 24, // 24 saatte bir session yenile
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 dakika önbellek — DB yükünü azaltır
},
},
cookieCache etkinleştirildiğinde, session bilgisi HTTP-only cookie'de 5 dakika önbelleğe alınır. Bu süre içinde gelen istekler veritabanına gitmez — yüksek trafikli uygulamalarda önemli bir performans kazanımı.
Session'ı sunucu tarafından geçersiz kılmak için:
await auth.api.revokeSession({ token: sessionToken })
await auth.api.revokeUserSessions({ userId: "..." }) // Tüm oturumları kapat
Güvenlik Notları
HTTPS ve Cookie Güvenliği
Production'da Better Auth, BETTER_AUTH_URL HTTPS ile başlıyorsa session cookie'lerini otomatik olarak Secure flag'iyle işaretler. Geliştirme ortamında HTTP sorunsuz çalışır.
CSRF Koruması
Better Auth, tüm state-modifying endpoint'leri (sign-in, sign-out, vb.) için built-in CSRF koruması sunar. Ekstra bir şey yapmanıza gerek yok.
Parolaların Saklanması
Email/şifre kimlik doğrulamasında parolalar, argon2id algoritmasıyla hash'lenerek veritabanına kaydedilir. Ham parolalar hiçbir zaman saklanmaz.
Production Kontrol Listesi
Uygulamanızı canlıya almadan önce:
-
BETTER_AUTH_SECRETproduction'da güçlü ve eşsiz bir değer -
BETTER_AUTH_URLdoğru domain (örn.https://www.example.com) - Magic link URL'lerinin gönderilen domain ile eşleştiğini doğrulayın
- OAuth callback URL'lerini provider konsollarında kaydedin
- Email gönderimi için alan adı doğrulaması tamamlandı (SPF/DKIM)
-
proxy.tsile korunan rotalar test edildi - Rol tabanlı erişim kontrolleri uçtan uca test edildi
- Session timeout ve revocation senaryoları test edildi
- 2FA akışı eksiksiz test edildi (varsa)
Sıkça Sorulan Sorular
Better Auth ücretsiz mi?
Evet, Better Auth tamamen ücretsiz ve açık kaynaklı. Tüm özellikler ve 50'yi aşkın plugin MIT lisansıyla sunuluyor. Kullanıcı başına ücret yok, self-hosted çalışıyor. Ticari destek planları için resmi web sitesini inceleyin.
NextAuth'dan Better Auth'a geçiş nasıl yapılır?
Temel adımlar şunlardır: mevcut veritabanı tablolarınızı Better Auth şemasına taşıma, /api/auth/[...nextauth] handler'ı /api/auth/[...all] ile değiştirme, ve client tarafında useSession import path'ini güncelleme. Resmi migration kılavuzu için Better Auth belgelerine bakın.
Magic link ve şifre aynı projede birlikte kullanılabilir mi?
Evet. Bu projenin yaptığı tam olarak bu: admin paneli email/şifre, müşteri portalı magic link kullanıyor. Her iki yöntem aynı user tablosunu paylaşır; kullanıcı kaydı hangi yöntemle olursa olsun aynı tabloya düşer.
Better Auth, Edge Runtime'da çalışır mı?
Drizzle adapter ile birlikte Edge Runtime desteklenmez — Node.js runtime gerektirir. Bu nedenle proxy.ts Node.js üzerinde çalışır (Next.js 16'nın varsayılan davranışıdır). Edge ihtiyacınız varsa Better Auth'un JWT stratejisi veya harici session store değerlendirilebilir.
Çok kiracılı (multi-tenant) yapılar için Better Auth uygun mu?
Evet, Better Auth'un Organization plugin'i tam multi-tenant desteği sunar: takım/organizasyon oluşturma, üye davetleri, rol yönetimi (owner, admin, member). Kurumsal uygulamalar için yeterince güçlü bir yapı sunar.
Rate limiting nasıl çalışıyor?
Better Auth, built-in rate limiting içerir. Başarısız giriş denemelerini otomatik sınırlar. Ek konfigürasyon ihtiyaçları için özel Rate Limiting plugin'i de mevcuttur.
Sonuç
Better Auth, 2026 itibarıyla Next.js projelerinde kimlik doğrulama için olgun ve güvenilir bir seçenek. TypeScript-first yaklaşımı, resmi Drizzle adapter'ı ve Next.js 16'nın proxy.ts özelliğiyle sorunsuz entegrasyonu, Drizzle kullanan projelerde onu özellikle cazip kılıyor.
Bu projede hem admin paneli hem müşteri portalı Better Auth üzerinde production'da çalışıyor. Magic link ile şifresiz müşteri girişi, email/şifre ile admin erişimi — Better Auth her iki senaryoyu aynı codebase içinde temiz biçimde yönetiyor.
Modern Next.js stack'inin diğer parçalarını merak ediyorsanız Drizzle ORM rehberimize veya PayTR ödeme entegrasyonu rehberimize göz atabilirsiniz.

