· 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 Ethan
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@latestlà đủ) - Một database Neon (tier miễn phí được — bạn lấy
DATABASE_URLtừ 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 Drizzledrizzle-orm+@neondatabase/serverless— Drizzle + Neon driver cho serverless Postgresdrizzle-kit— CLI để tạo và chạy migrations
Lỗi thường gặp: Cài
better-authmà 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_URLkhi 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-authtrự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
- Vào GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
- Điền:
- Application name: tùy ý
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Click Register application, sau đó copy Client ID và tạo Client Secret
- 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:emailthủ công, nếu khônggithub.profile.emailsẽ lànullvà 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ị
errortrả về từsignIn.email/signUp.email. Các hàm này không throw — chúng trả về{ data, error }. Bỏ quaerrorđồ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ậnPromise<ReadonlyHeaders>thay vìReadonlyHeaders, và Better Auth không đọc được cookie. Session trả vềnullmọ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ànulltrong 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ùnggetSessionCookie(request)từbetter-auth/cookiescho 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_SECRETBETTER_AUTH_URL(trỏ về domain production của bạn)DATABASE_URLGITHUB_CLIENT_IDGITHUB_CLIENT_SECRETNEXT_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.