Auth.js v5 (NextAuth) в Next.js 15: Пълно ръководство за self-hosted автентикация с Drizzle (2026)

Как да настроите Auth.js v5 (NextAuth) в Next.js 15 със self-hosted автентикация, Drizzle adapter, OAuth провайдъри, JWT сесии и защитени маршрути с middleware.

Актуализирано: 18 август 2026 г.

Auth.js v5 (бившият NextAuth.js) е официалната библиотека за автентикация за Next.js 15, която добавя поддръжка на App Router, Server Components и Edge Runtime, като запазва изцяло self-hosted модела без принудителна зависимост от външен доставчик. В това ръководство ще покажа как настройвам Auth.js v5 със Drizzle ORM, как избирам между JWT и database сесии в реален проект и как защитавам маршрути с middleware. Ще споделя и грешките, които сам направих по пътя.

  • Auth.js v5 е новото име на NextAuth.js от октомври 2023 г. Пакетът в npm се казва next-auth (версия 5.x). Това не е нова библиотека, а нова главна версия със съществено различен API.
  • Конфигурацията вече не е в [...nextauth].ts route handler. Има централизиран auth.ts файл, който експортира auth(), handlers, signIn и signOut.
  • JWT сесиите са единствената опция при чист middleware в Edge Runtime; database сесиите работят само в Node.js runtime.
  • Drizzle adapter (@auth/drizzle-adapter) поддържа PostgreSQL, MySQL и SQLite и е препоръчителният път за self-hosted setup, ако вече ползвате Drizzle.
  • Функцията auth() замества getServerSession и работи в Server Components, Server Actions, Route Handlers и middleware, с еднакъв API.
  • Credentials provider изисква ръчна имплементация на password hashing (bcrypt) и не създава автоматично записи в database. Трябва да ги правите с миграция или endpoint за регистрация.

Какво е Auth.js v5 и това ли е NextAuth?

Auth.js и NextAuth.js са две имена за един и същ проект. През октомври 2023 г. поддръжниците преименуваха NextAuth.js на Auth.js, за да отразят, че библиотеката вече поддържа не само Next.js, но и SvelteKit, SolidStart, Express и други. За Next.js обаче npm пакетът остана next-auth, а версия 5 (next-auth@beta) е това, което по-нататък наричаме „Auth.js v5“. Ако видите пакет @auth/core, това е споделеното ядро между всички framework обвивки.

Основната разлика с v4 е, че v5 е проектирана около React Server Components. Целият конфигурационен модел се промени: няма повече [...nextauth]/route.ts с обвиване на NextAuth(authOptions). Вместо това създавате един auth.ts файл в корена на проекта, който експортира готови функции: auth() за четене на сесията, handlers за route handler-а, и signIn / signOut за Server Actions. Това ми спестява поне 40 реда boilerplate код в сравнение с v4.

Auth.js остава изцяло self-hosted. Данните и сесиите живеят във вашата база данни, не при трета страна. Това е основната причина да го предпочитам пред Clerk или Auth0 за проекти, където compliance екипът иска пълен контрол върху PII. За справка, официалният migration guide към v5 има изчерпателно описание на всяка промяна в API.

Инсталация и настройка на Auth.js v5 в Next.js 15

Първата стъпка е инсталация на next-auth в бета канала. Към средата на 2026 г. v5 все още е технически „beta“, макар и стабилна и препоръчана в новата документация:

pnpm add next-auth@beta
# ако използвате Drizzle:
pnpm add @auth/drizzle-adapter

След това създайте auth.ts в корена на проекта. Не в app/, защото ще го импортираме и от middleware, а middleware не бива да имат зависимости с Node.js API-та (bcrypt, database клиенти), които не работят на Edge Runtime. Ето минималния skeleton:

// auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [Google],
  session: { strategy: "jwt" },
  pages: {
    signIn: "/login",
  },
})

След това добавете route handler в app/api/auth/[...nextauth]/route.ts:

// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth"
// НЕ: import { handlers } from "@/auth"; export const { GET, POST } = handlers
// (второто също работи, но има издайнически трик. На v5 handlers вече е обект с методи,
// така че деструктуриране на root level дава по-чист diff при code review)

Забележете как v5 очаква prefix AUTH_ (не NEXTAUTH_) за всички environment variables. Това е автоматична промяна от v4 и една от най-честите причини за неработещи deploy-и след ъпгрейд. Ако имате legacy NEXTAUTH_SECRET, можете временно да я оставите, но новото име ще получи приоритет.

Как да добавите Google и GitHub OAuth провайдъри

OAuth конфигурацията в v5 е драстично опростена в сравнение с v4. За Google е достатъчно да зададете AUTH_GOOGLE_ID и AUTH_GOOGLE_SECRET в .env.local. Auth.js автоматично ще ги прочете. Не е нужно да ги подавате експлицитно в конфигурацията:

// auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Google, // чете AUTH_GOOGLE_ID и AUTH_GOOGLE_SECRET автоматично
    GitHub, // чете AUTH_GITHUB_ID и AUTH_GITHUB_SECRET
  ],
  session: { strategy: "jwt" },
})

Callback URL, който трябва да регистрирате в Google Cloud Console, е https://your-domain.com/api/auth/callback/google за production и http://localhost:3000/api/auth/callback/google за local development. За GitHub същия pattern, само с /github. Ако ползвате няколко environment-а (staging, preview), регистрирайте всеки callback URL отделно; wildcard-и не се поддържат.

Login бутон в клиентски компонент изглежда така:

// app/login/login-buttons.tsx
"use client"

import { signIn } from "next-auth/react"

export function LoginButtons() {
  return (
    <div className="flex flex-col gap-2">
      <button onClick={() => signIn("google", { redirectTo: "/dashboard" })}>
        Вход с Google
      </button>
      <button onClick={() => signIn("github", { redirectTo: "/dashboard" })}>
        Вход с GitHub
      </button>
    </div>
  )
}

За Server Action-и има отделен signIn от @/auth (не от next-auth/react). Не ги смесвайте. Първият е за server side, вторият за client side. Това е разлика от v4, където имаше единен signIn само за клиента.

Credentials provider с Drizzle adapter и bcrypt

OAuth покрива 80% от случаите, но много self-hosted проекти имат нужда и от email/password вход. Честно казано, Credentials provider в Auth.js v5 умишлено НЕ хешира парола вместо вас. Трябва да го направите ръчно. Ето цялостна конфигурация с Drizzle ORM и bcrypt:

// auth.ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import bcrypt from "bcryptjs"
import { db } from "@/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  session: { strategy: "jwt" }, // credentials изисква JWT
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Парола", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null

        const user = await db.query.users.findFirst({
          where: eq(users.email, credentials.email as string),
        })
        if (!user?.passwordHash) return null

        const valid = await bcrypt.compare(
          credentials.password as string,
          user.passwordHash
        )
        if (!valid) return null

        return { id: user.id, email: user.email, name: user.name }
      },
    }),
  ],
})

Drizzle adapter изисква специфични schema таблици: users, accounts, sessions, verificationTokens, authenticators. Официалната документация на Drizzle adapter в Auth.js има готова schema за PostgreSQL, MySQL и SQLite. Копирайте я директно и добавете passwordHash колона към users, ако използвате credentials.

JWT срещу database сесии: коя стратегия да изберете?

Това е решение с най-голямо архитектурно последствие в Auth.js. JWT сесиите съхраняват цялата информация за потребителя в signed cookie, така че сървърът не прави database заявка, за да разбере кой е логнат. Database сесиите държат само session id в cookie, а всичко останало се пази в database таблица sessions. Ето разликите, които влияят на реален проект:

Характеристика JWT сесии Database сесии
Работи в middleware (Edge) Да Не (изисква Node runtime)
Работи с Credentials provider Да Не
Незабавен logout (revoke) Не (чака expiry) Да (изтриване от DB)
Database заявки на request Нула Една SELECT за session
Размер на cookie ~800-1500 байта ~60 байта
Промяна на role без re-login Не (кеширано в JWT) Да (следваща заявка вижда)

В практиката ми: за B2C приложения с OAuth и хиляди активни сесии избирам JWT, защото database заявките за session lookup стават осезаеми при 500+ RPS. За B2B admin панели с credentials login и изискване за незабавен revoke на компрометиран акаунт избирам database сесии. И един компромис, ако имате нужда от и двете (JWT в middleware + revoke): добавете tokenVersion колона в users, включете я в JWT callback-а, и я проверявайте при чувствителни действия. Не е идеално, но работи.

Как да защитим маршрутите с middleware и auth()

Има два подхода за защита на маршрути: middleware-based (спира заявката преди да достигне компонент) и component-based (проверява в Server Component и redirect()-ва). За production използвам и двата: middleware за първата линия защита, компонентите за детайлни role checks.

За да работи middleware с Edge Runtime, разделете конфигурацията на два файла:

// auth.config.ts — САМО Edge-safe код (без bcrypt, без DB клиенти)
import type { NextAuthConfig } from "next-auth"

export const authConfig = {
  pages: { signIn: "/login" },
  providers: [], // празно тук, попълва се в auth.ts
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard")
      if (isOnDashboard) return isLoggedIn
      return true
    },
  },
} satisfies NextAuthConfig
// auth.ts — пълна конфигурация с DB и Node-only код
import NextAuth from "next-auth"
import { authConfig } from "./auth.config"
import Credentials from "next-auth/providers/credentials"
// ...bcrypt, Drizzle и т.н.

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [Credentials({ /* ... */ })],
})
// middleware.ts
import NextAuth from "next-auth"
import { authConfig } from "./auth.config"

export const { auth: middleware } = NextAuth(authConfig)

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

Middleware обаче не бива да е единствената защита. Изпълнява се на Edge, не вижда пълната сесия и може да бъде байпаснат при misconfig на matcher. Виждам често в code review-и как разработчици разчитат само на middleware и забравят да проверят в Server Component-а. За контекст относно ограниченията и best practices на middleware, вижте Middleware в Next.js 15.

Достъп до сесията в Server Components и Server Actions

Най-голямата новост в v5 е че auth() е универсална: работи в Server Components, Server Actions, Route Handlers, middleware, дори в edge runtime. Няма повече отделни функции getServerSession и getToken:

// app/dashboard/page.tsx
import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  const session = await auth()
  if (!session?.user) redirect("/login")

  return (
    <main>
      <h1>Здравей, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
    </main>
  )
}

За Server Action-и същият API, само че използвайте signOut вместо redirect за logout, за да се почистят cookies:

// app/actions/account.ts
"use server"

import { auth, signOut } from "@/auth"
import { db } from "@/db"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"

export async function updateProfile(name: string) {
  const session = await auth()
  if (!session?.user?.id) throw new Error("Не сте оторизиран")

  await db.update(users).set({ name }).where(eq(users.id, session.user.id))
  return { success: true }
}

export async function logout() {
  await signOut({ redirectTo: "/" })
}

За mutations с валидация препоръчвам да комбинирате Auth.js със Zod, както е показано в статията за Server Actions в Next.js 15. Схемата за проверка на права типично изглежда така: 1) await auth(), 2) валидация на input със Zod, 3) role check срещу database, 4) mutation.

Мигриране от NextAuth v4 към Auth.js v5

Ако мигрирате съществуващ проект от v4, ето списъкът с чупливи промени, които видях при последния си миграционен спринт:

  1. Environment variables: NEXTAUTH_SECRETAUTH_SECRET, NEXTAUTH_URLAUTH_URL. Auth.js v5 приема и стария префикс, но издава warning в билд лога.
  2. Централизиран auth.ts: изтрийте authOptions обекта от [...nextauth]/route.ts и го преместете в auth.ts в корена. Route handler-ът става еднa export ред.
  3. getServerSession → auth(): намерете и заменете глобално. Сигнатурата е същата: const session = await auth().
  4. signIn от Server Action: сега е валиден. Импортирайте от @/auth, не от next-auth/react. Използвайте го в <form action={...}> за progressive enhancement.
  5. Cookie имена: v5 сменя next-auth.session-token с authjs.session-token. Съществуващите сесии ще бъдат невалидирани. Планирайте кратък прозорец, в който всички потребители трябва да се логнат отново, или напишете script за migration на cookies.
  6. Adapter API: адаптерите (Drizzle, Prisma, MongoDB) са в отделен npm scope @auth/*, не @next-auth/*. Старият @next-auth/drizzle-adapter няма да работи с v5.

За production миграция препоръчвам двустъпков rollout: първо ъпгрейд на next-auth в staging environment с новите env variables, тест на всички login/logout flows, и чак тогава production deploy с ясно съобщение до потребителите. Пълен списък на breaking changes има в releases страницата на GitHub.

Често задавани въпроси

NextAuth deprecated ли е?

Не. NextAuth.js е преименуван на Auth.js през 2023 г. и продължава активно развитие. npm пакетът остава next-auth, а версия 5 (в момента в бета) е препоръчаната версия за Next.js 15 и App Router.

Мога ли да използвам Auth.js без database?

Да, ако използвате само OAuth провайдъри и JWT session strategy. Auth.js ще съхранява цялата сесия в signed cookie и няма да прави database заявки. Database adapter се изисква само за database сесии, credentials provider с постоянни потребители или magic link email flow.

Как работи Auth.js със Server Components?

Импортирате auth от вашия @/auth файл и извиквате await auth() директно в async Server Component. Функцията чете сесията от cookies и връща { user, expires } или null. Няма нужда от SessionProvider или client-side context за server-only маршрути.

Каква е разликата между Auth.js и Clerk?

Auth.js е open-source, self-hosted библиотека, а данните на потребителите живеят във вашата база. Clerk е managed SaaS с UI компоненти извън кутията, но потребителските данни се пазят при тях и има месечна цена след безплатен tier. За compliance-чувствителни приложения (fintech, healthcare) Auth.js обикновено е по-подходящ избор.

Защо auth() връща null дори когато съм логнат?

Най-честите причини: липсва AUTH_SECRET environment variable, cookie домейнът не съвпада (например заявка от www.example.com когато cookie е за example.com), или използвате database сесии в middleware (не работи на Edge). Проверете next dev лога за warnings относно AUTH_ конфигурация.

За Автора Tomasz Wieczorek

Tomasz is a senior frontend engineer with eleven years of experience, currently a tech lead at a Berlin-based fintech where he migrated 180 routes from CRA to Next.js 14 App Router over nine months without a single user-facing outage. Before that, four years at Klarna building the merchant dashboard in Next.js, and two years at a small agency in Krakow shipping marketing sites for European retailers. He maintains two small open-source libraries in the React ecosystem, one for typed environment variables and one for a Suspense-friendly data fetching wrapper that predates the React 19 use() hook. He prefers Drizzle over Prisma, NextAuth over Clerk for self-hosted setups, and has strong feelings about putting business logic in Server Components. He writes the kind of tutorials that show the broken intermediate state, not just the working final code.