الكتابات/tutorial/2026/07
Tutorial8 يوليو 2026·30 دقيقة

Better Auth مع Next.js 15 App Router: دليل المصادقة الشامل

بناء نظام مصادقة متكامل في Next.js 15 باستخدام Better Auth — مكتبة TypeScript المستقلة عن الأطر والتي استحوذت عليها Vercel. يغطي البريد الإلكتروني وكلمة المرور وOAuth الاجتماعي والمسارات المحمية وإدارة الجلسات.

Better Auth هي مكتبة المصادقة التي استحوذت عليها Vercel بهدوء في يوليو 2026، وذلك لأسباب وجيهة. فهي مستقلة عن الأطر، وتعتمد TypeScript بشكل أساسي، وتأتي بميزات أكثر من أي منافس — بريد إلكتروني وكلمة مرور، OAuth، مصادقة ثنائية، إدارة جلسات، ونظام إضافات متنامٍ. يرشدك هذا الدليل خطوة بخطوة لبناء نظام مصادقة جاهز للإنتاج في مشروع Next.js 15 App Router من الصفر.

المتطلبات الأساسية

قبل البدء، تأكد من توفر ما يلي:

  • Node.js 20 أو أحدث
  • قاعدة بيانات PostgreSQL (محلية عبر Docker أو مستضافة مثل Neon أو Supabase)
  • إلمام أساسي بـ Next.js App Router وTypeScript
  • تطبيق GitHub OAuth لتسجيل الدخول الاجتماعي (سنُنشئه أثناء الدليل)
  • pnpm مثبّت عالميًا (npm install -g pnpm)

ما ستبنيه

بنهاية هذا الدليل ستحصل على تطبيق Next.js 15 يتضمن:

  • تسجيل وتسجيل دخول بالبريد الإلكتروني وكلمة المرور
  • تسجيل دخول اجتماعي عبر GitHub OAuth
  • إدارة جلسات مستندة إلى JWT مع تجديد تلقائي
  • وصول إلى الجلسة من جانب الخادم في React Server Components
  • حماية المسارات عبر Middleware
  • تدفق تسجيل خروج مع إعادة توجيه

الخطوة 1: إنشاء مشروع Next.js 15

ابدأ بمشروع Next.js 15 جديد مع App Router وTypeScript:

pnpm create next-app@latest my-auth-app --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"
cd my-auth-app

بعد اكتمال العملية، افتح المشروع في المحرر.

الخطوة 2: تثبيت Better Auth والمكتبات المطلوبة

ثبّت Better Auth مع Drizzle ORM وعميل PostgreSQL:

pnpm add better-auth drizzle-orm @auth/drizzle-adapter
pnpm add pg
pnpm add -D drizzle-kit @types/pg

أنشئ ملف .env.local بالمتغيرات المطلوبة. تحتاج Better Auth إلى مفتاح سري لتوقيع الجلسات:

# توليد مفتاح قوي
openssl rand -base64 32

أضف التالي إلى .env.local:

# قاعدة البيانات
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp
 
# Better Auth
BETTER_AUTH_SECRET=المفتاح-الذي-ولّدته
BETTER_AUTH_URL=http://localhost:3000
 
# GitHub OAuth (الخطوة 11)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

الخطوة 3: إعداد قاعدة البيانات مع Drizzle ORM

أنشئ ملف إعداد Drizzle في جذر المشروع:

// drizzle.config.ts
import type { Config } from "drizzle-kit";
 
export default {
  schema: "./lib/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;

أنشئ اتصال قاعدة البيانات:

// lib/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL!,
});
 
export const db = drizzle(pool);

تستطيع Better Auth توليد مخططها تلقائيًا. شغّل أداة CLI لإنتاج ملفات الهجرة:

pnpm dlx better-auth generate --output ./lib/db/schema.ts

ينشئ هذا جداول user وsession وaccount وverification. ثم طبّق الهجرة:

pnpm drizzle-kit push

الخطوة 4: إعداد خادم Better Auth

أنشئ ملف إعداد المصادقة المركزي. هنا تربط قاعدة البيانات وتُفعّل الموفرين وتضبط الخيارات:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/lib/db";
 
export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false, // اضبطها على true في الإنتاج
    minPasswordLength: 8,
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 أيام بالثواني
    updateAge: 60 * 60 * 24,      // تجديد إذا كانت أقدم من يوم
  },
});

تُعيد دالة betterAuth نسخة مصادقة مُهيَّأة بالكامل تتولى كل التشفير ودوران الرموز وعمليات قاعدة البيانات.

الخطوة 5: إنشاء معالج مسار API

تستخدم Better Auth مسار API شامل لمعالجة جميع نقاط نهاية المصادقة. أنشئ هذا الملف:

// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
 
export const { GET, POST } = toNextJsHandler(auth);

يتولى هذا الملف الواحد تلقائيًا معالجة جميع نقاط النهاية:

  • POST /api/auth/sign-up/email
  • POST /api/auth/sign-in/email
  • POST /api/auth/sign-out
  • GET /api/auth/session
  • GET /api/auth/callback/github
  • وأكثر

الخطوة 6: تهيئة عميل المصادقة

تُغلّف نسخة العميل جميع إجراءات المصادقة للاستخدام في مكوّنات React. أنشئها مرة واحدة واستوردها أينما احتجت:

// 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,
  getSession,
} = authClient;

أضف أيضًا NEXT_PUBLIC_APP_URL=http://localhost:3000 إلى .env.local.

الخطوة 7: بناء صفحة التسجيل

أنشئ صفحة تسجيل تجمع اسم المستخدم والبريد الإلكتروني وكلمة المرور:

// app/(auth)/sign-up/page.tsx
"use client";
 
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signUp } from "@/lib/auth-client";
 
export default function SignUpPage() {
  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 data = new FormData(e.currentTarget);
    const name = data.get("name") as string;
    const email = data.get("email") as string;
    const password = data.get("password") as string;
 
    const { error: signUpError } = await signUp.email({
      name,
      email,
      password,
      callbackURL: "/dashboard",
    });
 
    if (signUpError) {
      setError(signUpError.message ?? "فشل التسجيل. حاول مجدداً.");
      setLoading(false);
      return;
    }
 
    router.push("/dashboard");
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="w-full max-w-md bg-white rounded-2xl shadow p-8">
        <h1 className="text-2xl font-bold mb-6">إنشاء حساب جديد</h1>
 
        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">الاسم</label>
            <input
              name="name"
              type="text"
              required
              className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
          <div>
            <label className="block text-sm font-medium mb-1">البريد الإلكتروني</label>
            <input
              name="email"
              type="email"
              required
              className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
          <div>
            <label className="block text-sm font-medium mb-1">كلمة المرور</label>
            <input
              name="password"
              type="password"
              required
              minLength={8}
              className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
 
          {error && (
            <p className="text-red-600 text-sm">{error}</p>
          )}
 
          <button
            type="submit"
            disabled={loading}
            className="w-full bg-blue-600 text-white rounded-lg py-2 font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? "جارٍ الإنشاء…" : "إنشاء حساب"}
          </button>
        </form>
 
        <p className="mt-4 text-sm text-center text-gray-600">
          لديك حساب بالفعل؟{" "}
          <Link href="/sign-in" className="text-blue-600 hover:underline">
            تسجيل الدخول
          </Link>
        </p>
      </div>
    </div>
  );
}

الخطوة 8: بناء صفحة تسجيل الدخول

صفحة تسجيل الدخول تتبع النمط ذاته، وتتضمن أيضًا زر GitHub OAuth:

// app/(auth)/sign-in/page.tsx
"use client";
 
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signIn } from "@/lib/auth-client";
 
export default function SignInPage() {
  const router = useRouter();
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
 
  async function handleEmailSignIn(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setLoading(true);
    setError(null);
 
    const data = new FormData(e.currentTarget);
 
    const { error: signInError } = await signIn.email({
      email: data.get("email") as string,
      password: data.get("password") as string,
      callbackURL: "/dashboard",
    });
 
    if (signInError) {
      setError(signInError.message ?? "بيانات اعتماد غير صحيحة.");
      setLoading(false);
      return;
    }
 
    router.push("/dashboard");
  }
 
  async function handleGitHubSignIn() {
    await signIn.social({
      provider: "github",
      callbackURL: "/dashboard",
    });
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="w-full max-w-md bg-white rounded-2xl shadow p-8">
        <h1 className="text-2xl font-bold mb-6">تسجيل الدخول إلى حسابك</h1>
 
        <button
          onClick={handleGitHubSignIn}
          className="w-full flex items-center justify-center gap-2 border rounded-lg py-2 mb-4 hover:bg-gray-50"
        >
          <svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
            <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
          </svg>
          المتابعة عبر GitHub
        </button>
 
        <div className="relative my-4">
          <div className="absolute inset-0 flex items-center">
            <span className="w-full border-t" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white px-2 text-gray-500">أو</span>
          </div>
        </div>
 
        <form onSubmit={handleEmailSignIn} className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">البريد الإلكتروني</label>
            <input
              name="email"
              type="email"
              required
              className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
          <div>
            <label className="block text-sm font-medium mb-1">كلمة المرور</label>
            <input
              name="password"
              type="password"
              required
              className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>
 
          {error && (
            <p className="text-red-600 text-sm">{error}</p>
          )}
 
          <button
            type="submit"
            disabled={loading}
            className="w-full bg-blue-600 text-white rounded-lg py-2 font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? "جارٍ تسجيل الدخول…" : "تسجيل الدخول"}
          </button>
        </form>
 
        <p className="mt-4 text-sm text-center text-gray-600">
          لا يوجد لديك حساب؟{" "}
          <Link href="/sign-up" className="text-blue-600 hover:underline">
            التسجيل
          </Link>
        </p>
      </div>
    </div>
  );
}

الخطوة 9: حماية المسارات عبر Middleware

استخدم Next.js middleware لإعادة توجيه المستخدمين غير المصادق عليهم بعيدًا عن الصفحات المحمية:

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
 
export async function middleware(request: NextRequest) {
  const sessionCookie = getSessionCookie(request);
  const { pathname } = request.nextUrl;
 
  // إعادة توجيه المستخدمين المسجّلين بعيدًا عن صفحات المصادقة
  if (sessionCookie && ["/sign-in", "/sign-up"].includes(pathname)) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }
 
  // حماية مسارات لوحة التحكم
  if (!sessionCookie && pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/sign-in", request.url));
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/dashboard/:path*", "/sign-in", "/sign-up"],
};

المساعد getSessionCookie خفيف الوزن عن قصد — يقرأ قيمة الكوكي دون استدعاء قاعدة البيانات، لذا يعمل Middleware في أقل من 1 ملّي ثانية على الحافة.

الخطوة 10: الوصول إلى الجلسة في Server Components

في أي React Server Component، يمكنك جلب الجلسة الكاملة من قاعدة البيانات:

// 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("/sign-in");
  }
 
  return (
    <main className="min-h-screen p-8">
      <h1 className="text-3xl font-bold">
        مرحبًا، {session.user.name}
      </h1>
      <p className="text-gray-500 mt-2">{session.user.email}</p>
    </main>
  );
}

الخطوة 11: إضافة GitHub OAuth

لقد هيّأت GITHUB_CLIENT_ID وGITHUB_CLIENT_SECRET في الخطوة 4. أنشئ الآن تطبيق GitHub OAuth:

  1. اذهب إلى GitHub Settings ثم Developer settings ثم OAuth Apps ثم New OAuth App
  2. اضبط Homepage URL على http://localhost:3000
  3. اضبط Authorization callback URL على http://localhost:3000/api/auth/callback/github
  4. انسخ Client ID وأنشئ Client secret
  5. الصق القيمتين في .env.local

زر تسجيل الدخول عبر GitHub الذي أضفته في الخطوة 8 يستدعي signIn.social({ provider: "github" }) بالفعل. تتولى Better Auth تدفق OAuth بالكامل.

الخطوة 12: إضافة زر تسجيل الخروج

أضف زر تسجيل خروج إلى لوحة التحكم أو شريط التنقل:

// components/SignOutButton.tsx
"use client";
 
import { useRouter } from "next/navigation";
import { signOut } from "@/lib/auth-client";
 
export function SignOutButton() {
  const router = useRouter();
 
  async function handleSignOut() {
    await signOut({
      fetchOptions: {
        onSuccess: () => {
          router.push("/sign-in");
        },
      },
    });
  }
 
  return (
    <button
      onClick={handleSignOut}
      className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-red-600"
    >
      تسجيل الخروج
    </button>
  );
}

الخطوة 13: الوصول إلى الجلسة في Client Components

للمكوّنات التي تحتاج إلى الاستجابة لحالة المصادقة، استخدم خطاف useSession:

// components/UserAvatar.tsx
"use client";
 
import { useSession } from "@/lib/auth-client";
 
export function UserAvatar() {
  const { data: session, isPending } = useSession();
 
  if (isPending) {
    return <div className="w-8 h-8 rounded-full bg-gray-200 animate-pulse" />;
  }
 
  if (!session) {
    return null;
  }
 
  return (
    <div className="flex items-center gap-2">
      {session.user.image ? (
        <img
          src={session.user.image}
          alt={session.user.name}
          className="w-8 h-8 rounded-full"
        />
      ) : (
        <div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
          {session.user.name.charAt(0).toUpperCase()}
        </div>
      )}
      <span className="text-sm font-medium">{session.user.name}</span>
    </div>
  );
}

اختبار التطبيق

شغّل خادم التطوير وتحقق من كل تدفق:

pnpm dev

تسجيل بالبريد الإلكتروني: انتقل إلى http://localhost:3000/sign-up، أكمل النموذج، وأكّد أنك تُعاد إلى /dashboard.

تسجيل الخروج: انقر زر تسجيل الخروج وتحقق من الوصول إلى /sign-in.

تسجيل الدخول: عد إلى http://localhost:3000/sign-in وسجّل الدخول بالبيانات التي أنشأتها للتو.

حماية المسارات: بينما أنت غير مسجّل، انتقل مباشرة إلى http://localhost:3000/dashboard — يجب أن تُعاد إلى /sign-in.

GitHub OAuth: انقر المتابعة عبر GitHub، فوّض التطبيق، وأكّد إعادة التوجيه إلى /dashboard.

استكشاف الأخطاء وإصلاحها

BETTER_AUTH_SECRET مفقود: يتوقف الخادم عند بدء التشغيل إذا لم يُضبط المفتاح. تأكد من تحميل .env.local وتطابق اسم المتغير.

عدم تطابق عنوان callback في OAuth: تُعيد GitHub خطأ redirect_uri_mismatch إذا لم يتطابق عنوان callback بدقة. للتطوير المحلي استخدم http://localhost:3000/api/auth/callback/github.

عدم تجديد الجلسة: تُجدّد Better Auth الجلسة فقط إذا كانت أقدم من updateAge ثانية. في التطوير يمكنك ضبط updateAge: 0 لإجبار التجديد في كل طلب.

الخطوات التالية

  • تفعيل التحقق من البريد الإلكتروني: اضبط requireEmailVerification: true ونفّذ sendVerificationEmail مع خدمة بريد إلكتروني مثل Resend أو Mailgun.
  • إضافة المصادقة الثنائية: تحتوي Better Auth على إضافة 2FA مدمجة. ثبّت @better-auth/2fa وأضفه إلى مصفوفة plugins في إعداد المصادقة.
  • التحكم في الوصول المستند إلى الدور: استخدم إضافة @better-auth/rbac لتعريف الأدوار والصلاحيات مباشرة في إعداد المصادقة.
  • النشر على Vercel: اضبط جميع متغيرات البيئة في إعدادات مشروع Vercel. تعمل Better Auth بشكل متكامل مع وظائف Vercel بدون خادم.

الخلاصة

أصبح لديك الآن نظام مصادقة متكامل مبني على Better Auth وNext.js 15. تولّت المكتبة جميع الأجزاء الصعبة — تجزئة كلمات المرور الآمنة، ودوران رموز الجلسات، والتحقق من حالة OAuth، وحماية CSRF — مما يتيح لك التركيز على الميزات الفريدة لتطبيقك. مع استمرار Vercel في الاستثمار في Better Auth بعد الاستحواذ، توقّع تكاملًا أعمق مع منظومة Vercel وانتشارًا أسرع في مجتمع TypeScript.