السلسلة: هذا الدرس جزء من سلسلة TanStack. اقرأ المقال الرئيسي.
وضع مصادقة صحيحة نادراً ما يكون الجزء المُسلّي. better-auth صارت في 2026 المكتبة المرجع في نظام TypeScript الحديث: framework-agnostic، جلسات آمنة افتراضياً، OAuth متعدد المزوّدين، magic links، 2FA، multi-tenant. مع TanStack Start وملحقها tanstackStartCookies، تتكامل في أقل من ساعة.
المتطلبات
- Node.js 22 LTS، npm 10+ أو pnpm 9+
- مشروع TanStack Start v1 شغّال مع TypeScript
- قاعدة بيانات (PostgreSQL، Neon، Supabase، أو D1 على Cloudflare)
- Drizzle ORM مثبَّت مع schema أوّلي
الخطوة 1 — تثبيت better-auth وتحضير Drizzle
npm install better-auth drizzle-orm
npm install pg
npm install -D drizzle-kit @types/pg
# .env
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
السرّ يجب أن يكون طويلاً وغير متوقَّع. في الإنتاج، السرّ في متغيّرات بيئة المضيف.
الخطوة 2 — إعداد better-auth مع Drizzle adapter
// src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { db } from '~/db/client'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
emailAndPassword: { enabled: true },
plugins: [tanstackStartCookies()], // يجب أن يبقى آخراً
})
المعامل provider يدلّ على dialect SQL: pg لـ PostgreSQL، mysql، أو sqlite. لـ D1، استخدم sqlite مع client Drizzle مهيَّأ عبر drizzle-orm/d1.
الخطوة 3 — توليد جداول المصادقة
better-auth ينشئ آلياً أربعة جداول: user، session، account، verification. الـ schema يُولَّد عبر CLI:
npx @better-auth/cli generate --output ./src/db/auth-schema.ts
// src/db/schema.ts
export * from './auth-schema'
// ... جداولك الأخرى
npx drizzle-kit generate
npx drizzle-kit migrate
الخطوة 4 — كشف route catch-all /api/auth/$
better-auth يوفّر handler واحد يُجيب على كل routes /api/auth/*. نكشفه عبر route catch-all TanStack Router.
// src/routes/api/auth/$.ts
import { auth } from '~/lib/auth'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => auth.handler(request),
POST: async ({ request }: { request: Request }) => auth.handler(request),
},
},
})
الخطوة 5 — إنشاء client React لـ signIn/signUp/signOut
// src/lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_AUTH_URL,
})
export const { signIn, signUp, signOut, useSession } = authClient
hook useSession() يُرجع { data, isPending, error } ويُحدَّث آلياً بعد signIn أو signOut.
الخطوة 6 — بناء نموذج تسجيل
// src/routes/(auth)/signup.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'
import { signUp } from '~/lib/auth-client'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
})
export const Route = createFileRoute('/(auth)/signup')({
component: SignupPage,
})
function SignupPage() {
const router = useRouter()
const form = useForm({
defaultValues: { email: '', password: '', name: '' },
validators: { onChange: schema, onSubmit: schema },
onSubmit: async ({ value }) => {
const { error } = await signUp.email(value)
if (error) throw new Error(error.message)
router.navigate({ to: '/dashboard' })
},
})
return (
<form onSubmit={(e) => { e.preventDefault(); void form.handleSubmit() }}>
{/* form.Field لـ email، password، name */}
</form>
)
}
signUp.email(value) يرسل البيانات إلى /api/auth/sign-up/email؛ better-auth ينشئ المستخدم، يُجزّئ كلمة السرّ بـ scrypt افتراضياً، يفتح جلسة، ويُرجع cookie.
الخطوة 7 — استرداد الجلسة جانب الخادم
// src/lib/auth-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { auth } from './auth'
export const getSession = createServerFn({ method: 'GET' }).handler(async () => {
const headers = getRequestHeaders()
return await auth.api.getSession({ headers })
})
getRequestHeaders() يستردّ headers الطلب الحالي، منها cookie الجلسة. better-auth يتفحّصها، يتحقّق من التوقيع، يحمّل المستخدم من جدول session ويُرجع { user, session } أو null.
الخطوة 8 — حماية منطقة من التطبيق
// src/routes/_protected.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { getSession } from '~/lib/auth-functions'
export const Route = createFileRoute('/_protected')({
beforeLoad: async ({ location }) => {
const session = await getSession()
if (!session) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
return { user: session.user }
},
component: () => <Outlet />,
})
أي route تحت src/routes/_protected/ ترث هذه الحماية. return { user } يجعل المستخدم متاحاً في context الـ router.
الخطوة 9 — إضافة OAuth provider (GitHub)
لـ GitHub، أنشئ OAuth App في إعدادات حساب GitHub بـ URL callback http://localhost:3000/api/auth/callback/github. خذ Client ID وSecret في .env.
// src/lib/auth.ts (مقتطف مع GitHub)
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { db } from '~/db/client'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [tanstackStartCookies()],
})
import { signIn } from '~/lib/auth-client'
<button
onClick={() => signIn.social({ provider: 'github', callbackURL: '/dashboard' })}
>
Continuer avec GitHub
</button>
نفس الآلية تنطبق على Google، Apple، Discord، X، Microsoft، Facebook — better-auth يدعم 20+ مزوّداً في 2026.
الخطوة 10 — تسجيل خروج وتنظيف
import { signOut } from '~/lib/auth-client'
import { useRouter } from '@tanstack/react-router'
function LogoutButton() {
const router = useRouter()
return (
<button
onClick={async () => {
await signOut()
router.navigate({ to: '/' })
}}
>
Se deconnecter
</button>
)
}
better-auth يدير أيضاً rate limiting لمحاولات الاتصال وانتهاء صلاحية الجلسات الانزلاقي بلا إعداد إضافي.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| Cookie الجلسة مفقود | ملحق tanstackStartCookies مفقود |
أضفه آخراً في plugins |
| Tables not found | migrations Drizzle لم تُطبَّق | أطلق drizzle-kit generate ثم migrate |
| OAuth callback 404 | URL callback سيئ في المزوّد | تحقّق BETTER_AUTH_URL وcallback في GitHub/Google |
| الجلسة لا تنجو من refresh | cookie domain أو secure سيء | في prod، استخدم HTTPS واضبط cookieOptions |
| خطأ CORS على OAuth | baseURL لا يطابق Origin |
وَفّق BETTER_AUTH_URL مع URL التطبيق |
signIn.social لا يُطلق إعادة توجيه |
Provider غير مُعلَن في socialProviders | أضفه في auth.ts |
أسئلة شائعة
هل server function لكل استدعاء auth؟ لا، client better-auth/react يكشفها (signIn، signUp، signOut، updateUser). احتفظ بـ server functions للأعمال التي تتطلب المستخدم الحالي.
كيف نُفعّل 2FA؟ أضف ملحق twoFactor() في plugins: [...]. Client يكشف authClient.twoFactor.enable() الذي يُرجع QR code.
هل يمكن تخصيص جدول user (role، orgId)؟ نعم، عبر user: { additionalFields: { role: { type: 'string' } } }. better-auth يضيف هذه الأعمدة عند التوليد التالي.
كيف ندير organisations multi-tenant؟ استخدم ملحق organization() الذي ينشئ جداول organization، member، invitation.
هل better-auth يشتغل على Cloudflare Workers؟ نعم، شريطة استخدام client Drizzle متوافق (D1 أو Hyperdrive لـ PostgreSQL).