· nextjs / better-auth / authentication

Cách thêm Better Auth vào ứng dụng Next.js — từng bước một

Hướng dẫn thực tế cài đặt Better Auth v1.6.14 với Drizzle ORM, Neon Postgres và GitHub OAuth trên Next.js 15 App Router — bao gồm mọi lỗi phổ biến đáng lưu ý.

Bởi

2.009 từ · 11 phút đọc

Better Auth là thư viện bạn tìm đến khi NextAuth với config chuỗi ma thuật không còn đủ, nhưng bạn cũng không muốn tự viết session management. Hướng dẫn này kết nối mọi thứ từ đầu đến cuối: GitHub OAuth, email/password, Drizzle + Neon để lưu trữ, và bảo vệ route bằng middleware. Tất cả trên Next.js 15 App Router. Nếu bạn đang cân nhắc giữa Better Auth và Clerk, xem Better Auth vs Clerk trước khi bắt đầu.

Stack: Better Auth v1.6.14 · Next.js 15 · Drizzle ORM · Neon (serverless Postgres) · GitHub OAuth

Dành cho ai

Developer đang xây dựng dự án Next.js 15 App Router, muốn auth dựa trên session với database thực và ít nhất một OAuth provider. Nếu bạn đang dùng Pages Router hoặc hài lòng với auth SaaS bên thứ ba, hướng dẫn này không phải dành cho bạn.

Yêu cầu trước khi bắt đầu

  • Node.js 20+
  • Một dự án Next.js 15 App Router đã có sẵn (npx create-next-app@latest là đủ)
  • Một database Neon (tier miễn phí được — bạn lấy DATABASE_URL từ dashboard; xem so sánh các dịch vụ host Postgres nếu bạn chưa chọn)
  • Tài khoản GitHub để đăng ký OAuth App

Bước 1: Cài đặt các package

npm install better-auth@^1.6.14 @better-auth/drizzle-adapter drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

Từng package làm gì:

  • better-auth — thư viện auth cốt lõi
  • @better-auth/drizzle-adapter — kết nối các bảng session/user của Better Auth với Drizzle
  • drizzle-orm + @neondatabase/serverless — Drizzle + Neon driver cho serverless Postgres
  • drizzle-kit — CLI để tạo và chạy migrations

Lỗi thường gặp: Cài better-auth mà không có adapter, rồi truyền thẳng Drizzle client, sẽ gây ra lỗi “adapter not found” khó hiểu lúc runtime. Luôn cài cả hai cùng nhau.


Bước 2: Thiết lập biến môi trường

Tạo hoặc cập nhật .env.local:

# Bắt buộc với Better Auth
BETTER_AUTH_SECRET="your-random-secret-at-least-32-chars"
BETTER_AUTH_URL="http://localhost:3000"   # production: dùng URL thực của bạn

# Chuỗi kết nối Neon (copy từ Neon dashboard → Connection details)
DATABASE_URL="postgresql://user:[email protected]/neondb?sslmode=require"

# Thông tin GitHub OAuth App (xem Bước 7)
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"

BETTER_AUTH_SECRET dùng để ký session. Tạo một giá trị ngẫu nhiên bằng:

openssl rand -base64 32

Lỗi thường gặp: Quên cập nhật BETTER_AUTH_URL khi lên production là lỗi deploy phổ biến nhất. Better Auth dùng giá trị này để tạo OAuth callback URL — nếu vẫn trỏ về localhost, callback của GitHub sẽ redirect về đó và thất bại im lặng.


Bước 3: Tạo lib/auth.ts (server-side instance)

// lib/auth.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
import { nextCookies } from 'better-auth/next-js';
import { db } from '@/lib/db';   // Drizzle db instance của bạn

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg',
  }),
  plugins: [nextCookies()],
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  emailAndPassword: {
    enabled: true,
  },
});

Plugin nextCookies() giúp cookie hoạt động trong Server Actions và Route Handlers. Thiếu nó, auth.api.getSession() trả về null trong actions dù người dùng đã đăng nhập.

Lỗi thường gặp: Import auth ở phía client — trong component hoặc file có use client — sẽ throw error vì nó kéo theo các module chỉ chạy trên Node.js. Giữ file này là server-only. Nếu cần dùng chung kiểu dữ liệu, import từ better-auth trực tiếp, không phải từ lib/auth.ts.


Bước 4: Tạo lib/auth-client.ts (client-side instance)

// lib/auth-client.ts
import { createAuthClient } from 'better-auth/react';

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
});

export const { signIn, signUp, signOut, useSession } = authClient;

Thêm NEXT_PUBLIC_APP_URL vào .env.local nếu bạn muốn base URL có thể cấu hình theo từng môi trường:

NEXT_PUBLIC_APP_URL="http://localhost:3000"

Lỗi thường gặp: Dùng process.env.BETTER_AUTH_URL ở đây thay vì biến có tiền tố NEXT_PUBLIC_. Next.js loại bỏ các env var không có tiền tố public khi build bundle cho client. Client sẽ âm thầm fallback về URL sai.


Bước 5: Thêm route handler

Better Auth cần một catch-all route để xử lý tất cả các flow auth (OAuth callbacks, session API, credential endpoints).

// app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';

export const { GET, POST } = toNextJsHandler(auth);

Chỉ vậy thôi. toNextJsHandler wrap fetch handler của Better Auth để khớp với interface của Next.js Route Handler.

Lỗi thường gặp: Đặt file này tại app/api/auth/route.ts (không có [...all]) chỉ bắt được /api/auth — mọi OAuth callback và sub-route đều trả về 404. [...all] catch-all là bắt buộc.


Bước 6: Tạo database schema và chạy migrations

Better Auth tự tạo Drizzle schema cho các bảng users, sessions và accounts. Nếu bạn chưa quen với Drizzle, hướng dẫn cài đặt Drizzle ORM với Postgres bao gồm phần cấu hình chi tiết hơn. Chạy:

npx auth@latest generate

Lệnh này ghi ra một schema file (thường là lib/schema.ts hoặc tương tự — CLI sẽ thông báo). Sau đó chạy migration:

npx drizzle-kit push

Hoặc nếu bạn muốn dùng migration files thay vì push:

npx drizzle-kit generate
npx drizzle-kit migrate

Lỗi thường gặp: Chạy app trước khi migration. Better Auth sẽ cố ghi vào các bảng chưa tồn tại và throw lỗi Postgres đề cập đến bảng "session" hoặc "user" không tìm thấy. Luôn migrate trước.

Lỗi thường gặp: Nếu bạn đã tùy chỉnh Drizzle schema và schema do Better Auth tạo ra trùng tên bảng, sẽ có xung đột. Đổi tên bảng trong schema của bạn, không phải trong output của Better Auth — vì tái tạo sẽ ghi đè file của Better Auth.


Bước 7: Cấu hình GitHub OAuth App

  1. Vào GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
  2. Điền:
    • Application name: tùy ý
    • Homepage URL: http://localhost:3000
    • Authorization callback URL: http://localhost:3000/api/auth/callback/github
  3. Click Register application, sau đó copy Client ID và tạo Client Secret
  4. Dán cả hai vào .env.local (GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)

Khi deploy, đăng ký một OAuth App riêng (hoặc cập nhật cái đã có) với domain thực của bạn.

Lỗi thường gặp: Dùng GitHub App thay vì OAuth App. GitHub Apps không trả về email của user theo mặc định — bạn phải thêm scope user:email thủ công, nếu không github.profile.email sẽ là null và user không thể đăng nhập. Nếu integration GitHub hiện tại của bạn là App, hãy chuyển sang OAuth App hoặc thêm scope đó.

Lỗi thường gặp: Quên cập nhật callback URL khi deploy. GitHub từ chối OAuth handshake nếu callback URL trong request không khớp với URL đã đăng ký.


Bước 8: Xây dựng giao diện auth phía client

Better Auth cung cấp React hooks để quản lý trạng thái session và các method cho tất cả auth flow.

Trạng thái session

// components/UserNav.tsx
'use client';
import { useSession, signOut } from '@/lib/auth-client';

export function UserNav() {
  const { data: session, isPending } = useSession();

  if (isPending) return <span>Đang tải…</span>;
  if (!session) return null;

  return (
    <div>
      <span>{session.user.email}</span>
      <button onClick={() => signOut()}>Đăng xuất</button>
    </div>
  );
}

Đăng nhập với GitHub

'use client';
import { signIn } from '@/lib/auth-client';

export function GitHubSignIn() {
  return (
    <button
      onClick={() => signIn.social({ provider: 'github', callbackURL: '/dashboard' })}
    >
      Tiếp tục với GitHub
    </button>
  );
}

Đăng ký và đăng nhập bằng email/password

'use client';
import { signIn, signUp } from '@/lib/auth-client';
import { useState } from 'react';

export function AuthForm({ mode }: { mode: 'signin' | 'signup' }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const fn = mode === 'signup' ? signUp.email : signIn.email;
    const { error } = await fn({ email, password, callbackURL: '/dashboard' });
    if (error) setError(error.message ?? 'Đã có lỗi xảy ra');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} required />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
      {error && <p>{error}</p>}
      <button type="submit">{mode === 'signup' ? 'Tạo tài khoản' : 'Đăng nhập'}</button>
    </form>
  );
}

Lỗi thường gặp: Không xử lý giá trị error trả về từ signIn.email / signUp.email. Các hàm này không throw — chúng trả về { data, error }. Bỏ qua error đồng nghĩa UI không bao giờ hiển thị phản hồi khi thông tin đăng nhập sai.


Bước 9: Đọc session trong Server Components

// app/dashboard/page.tsx
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session) redirect('/login');

  return <h1>Xin chào, {session.user.email}</h1>;
}

headers() là async trong Next.js 15 — bạn phải await nó. Đây là thay đổi breaking so với Next.js 14 nơi headers() là synchronous.

Lỗi thường gặp: Gọi auth.api.getSession({ headers: headers() }) mà không có await. TypeScript có thể không bắt lỗi này nếu tsconfig của bạn không strict — bạn sẽ nhận Promise<ReadonlyHeaders> thay vì ReadonlyHeaders, và Better Auth không đọc được cookie. Session trả về null mọi lúc, người dùng liên tục bị redirect về /login.

Lỗi thường gặp: Gọi getSession() mà không có nextCookies() trong auth plugins (Bước 3). Session sẽ luôn là null trong Server Components và Server Actions dù cookie đang tồn tại.


Bước 10: Bảo vệ route trong middleware

// middleware.ts
import { getSessionCookie } from 'better-auth/cookies';
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionCookie = getSessionCookie(request);

  if (!sessionCookie) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};

getSessionCookie đọc cookie mà không cần gọi database — an toàn cho Edge Runtime. Nếu bạn cần validate session đầy đủ (kiểm tra session expiry trong database, xác nhận user roles), dùng auth.api.getSession() thay thế, nhưng phải chuyển runtime sang Node.js:

// middleware.ts (Node.js runtime, validate đầy đủ)
export const runtime = 'nodejs';

Lỗi thường gặp: Dùng getCookieCache() của Better Auth trong Edge Runtime. Hàm này không tương thích với Edge và throw lúc runtime. Dùng getSessionCookie(request) từ better-auth/cookies cho Edge, hoặc chuyển sang Node.js runtime nếu cần toàn bộ API.

Bảo mật: Giữ phiên bản Next.js từ ≥15.2.3 trở lên. CVE-2025-29927 mô tả lỗ hổng bypass middleware trong các phiên bản cũ hơn — kẻ tấn công có thể bỏ qua toàn bộ middleware bằng cách tạo request với header x-middleware-subrequest. Kiểm tra session trong middleware là lớp bảo vệ route đầu tiên của bạn; trên phiên bản Next.js dễ bị tấn công, nó có thể bị vô hiệu hóa.


Lưu ý về thay đổi freshAge trong v1.6.0

Better Auth v1.6.0 đã thay đổi cách tính freshAge (độ tươi của session cho step-up auth). Trước đây nó đo từ lần hoạt động cuối; từ v1.6.0, nó đo từ thời điểm tạo session. Nếu bạn đang nâng cấp từ v1.5.x và dùng step-up auth flow (nhắc người dùng nhập lại mật khẩu trước các thao tác nhạy cảm), hãy xem lại giá trị freshAge — một session được tạo nhiều ngày trước có thể bị coi là cũ dù người dùng vẫn đang hoạt động gần đây.


Deploy lên Vercel

Vercel tự động nhận .env.local trong môi trường development. Với production, thiết lập các biến môi trường tương tự trong Vercel dashboard tại Project Settings → Environment Variables:

  • BETTER_AUTH_SECRET
  • BETTER_AUTH_URL (trỏ về domain production của bạn)
  • DATABASE_URL
  • GITHUB_CLIENT_ID
  • GITHUB_CLIENT_SECRET
  • NEXT_PUBLIC_APP_URL

Serverless driver của Neon hoạt động với cả Edge Functions và Node.js runtimes của Vercel mà không cần cấu hình thêm.


Tham khảo