الكتابات/tutorial/2026/02
Tutorial16 فبراير 2026·30 دقيقة

بناء تطبيق فوري مع Supabase و Next.js 15: الدليل الشامل

تعلّم كيفية بناء تطبيق full-stack فوري باستخدام Supabase و Next.js 15 App Router. يغطي هذا الدليل المصادقة وإعداد قاعدة البيانات و Row Level Security والاشتراكات الفورية.

أصبح Supabase البديل مفتوح المصدر الأول لـ Firebase، حيث يوفر واجهة خلفية متكاملة مع PostgreSQL والمصادقة والاشتراكات الفورية والتخزين — كل ذلك جاهز للاستخدام. عند دمجه مع Next.js 15 و App Router، تحصل على إطار عمل full-stack قوي يتعامل مع العرض من جانب الخادم و Server Actions والتفاعلات السلسة مع العميل.

في هذا الدليل، ستبني لوحة مهام تعاونية فورية — نسخة مصغرة من Trello حيث يمكن لعدة مستخدمين إضافة وتعديل وحذف المهام التي تتزامن فوريًا عبر جميع المتصفحات المتصلة. ستتعلم إتقان Supabase Auth و Row Level Security (RLS) و triggers قاعدة البيانات ونظام Realtime Broadcast.

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

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

  • Node.js 20+ مثبّت
  • حساب Supabase — مجاني على supabase.com
  • معرفة أساسية بـ React و TypeScript
  • إلمام بـ Next.js App Router (الصفحات، التخطيطات، Server Components)
  • محرر أكواد (يُنصح بـ VS Code)

ما الذي ستبنيه

لوحة مهام تعاونية بالميزات التالية:

  • مصادقة بالبريد الإلكتروني/كلمة المرور مع مسارات محمية
  • تحديثات فورية للمهام عبر جميع العملاء المتصلين
  • Row Level Security لضمان أن كل مستخدم يرى بياناته فقط
  • Server Components لتحميل البيانات الأولي
  • Server Actions للعمليات
  • Middleware لإدارة الجلسات

الخطوة 1: إنشاء مشروع Supabase

توجه إلى supabase.com/dashboard وأنشئ مشروعًا جديدًا. بمجرد جاهزية مشروعك، سجّل هذه القيم من Settings > API:

  • Project URL — عنوان URL لنسخة Supabase الخاصة بك
  • anon public — مفتاح آمن للاستخدام في جانب العميل
  • service_role — وصول المسؤول (حافظ على سريته)

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

اذهب إلى SQL Editor في لوحة تحكم Supabase ونفّذ الكود التالي:

-- إنشاء جدول الملفات الشخصية المرتبط بـ auth.users
create table public.profiles (
  id uuid references auth.users on delete cascade not null primary key,
  email text,
  display_name text,
  created_at timestamptz default now() not null
);
 
-- إنشاء جدول المهام
create table public.tasks (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references auth.users on delete cascade not null,
  title text not null,
  description text,
  status text default 'todo' check (status in ('todo', 'in_progress', 'done')),
  created_at timestamptz default now() not null,
  updated_at timestamptz default now() not null
);
 
-- تفعيل Row Level Security
alter table public.profiles enable row level security;
alter table public.tasks enable row level security;
 
-- الملفات الشخصية: يمكن للمستخدمين قراءة وتعديل ملفهم الشخصي
create policy "Users can view own profile"
  on public.profiles for select
  using (auth.uid() = id);
 
create policy "Users can update own profile"
  on public.profiles for update
  using (auth.uid() = id);
 
-- المهام: يمكن للمستخدمين إدارة مهامهم الخاصة
create policy "Users can view own tasks"
  on public.tasks for select
  using (auth.uid() = user_id);
 
create policy "Users can create tasks"
  on public.tasks for insert
  with check (auth.uid() = user_id);
 
create policy "Users can update own tasks"
  on public.tasks for update
  using (auth.uid() = user_id);
 
create policy "Users can delete own tasks"
  on public.tasks for delete
  using (auth.uid() = user_id);
 
-- إنشاء ملف شخصي تلقائيًا عند التسجيل
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, email, display_name)
  values (new.id, new.email, split_part(new.email, '@', 1));
  return new;
end;
$$ language plpgsql security definer;
 
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();
 
-- تحديث updated_at تلقائيًا
create or replace function public.update_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;
 
create trigger tasks_updated_at
  before update on public.tasks
  for each row execute function public.update_updated_at();

يُنشئ هذا المخطط:

  • جدول profiles يُملأ تلقائيًا عند تسجيل المستخدم
  • جدول tasks مع تتبع الحالة
  • سياسات RLS لضمان وصول كل مستخدم لبياناته فقط
  • Triggers للطوابع الزمنية التلقائية

الخطوة 3: إعداد البث الفوري (Realtime Broadcasting)

للتحديثات الفورية القابلة للتوسع، يوصي Supabase بـ Broadcast عبر triggers قاعدة البيانات بدلاً من نهج postgres_changes القديم. نفّذ هذا في SQL Editor:

-- Trigger البث لجدول المهام
create or replace function broadcast_task_changes()
returns trigger as $$
begin
  perform realtime.broadcast_changes(
    'tasks:updates',
    TG_OP,
    TG_OP,
    TG_TABLE_NAME,
    TG_TABLE_SCHEMA,
    NEW,
    OLD
  );
  return null;
end;
$$ language plpgsql security definer;
 
create trigger tasks_broadcast_trigger
  after insert or update or delete on public.tasks
  for each row execute function broadcast_task_changes();

الخطوة 4: تهيئة مشروع Next.js

npx create-next-app@latest task-board --typescript --tailwind --eslint --app --src-dir
cd task-board

ثبّت حزم Supabase:

npm install @supabase/supabase-js @supabase/ssr

أنشئ ملف .env.local:

NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

الخطوة 5: إنشاء عملاء Supabase

تحتاج إلى عميلين: واحد لجانب الخادم (Server Components, Server Actions, Middleware) وآخر للمتصفح.

عميل المتصفح

// src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
 
export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

عميل الخادم

// src/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
 
export async function createClient() {
  const cookieStore = await cookies()
 
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // تم الاستدعاء من Server Component — يمكن تجاهله
            // إذا كان الـ middleware يتولى تحديث الجلسات
          }
        },
      },
    }
  )
}

الخطوة 6: إضافة Middleware لإدارة الجلسات

يقوم الـ Middleware بتحديث جلسة المستخدم مع كل طلب، مما يمنع انتهاء صلاحية الرموز:

// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
 
export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })
 
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )
 
  const {
    data: { user },
  } = await supabase.auth.getUser()
 
  // إعادة توجيه المستخدمين غير المصادق عليهم إلى صفحة تسجيل الدخول
  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/signup') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }
 
  // إعادة توجيه المستخدمين المصادق عليهم بعيدًا عن صفحات المصادقة
  if (user && (request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/signup')) {
    const url = request.nextUrl.clone()
    url.pathname = '/dashboard'
    return NextResponse.redirect(url)
  }
 
  return supabaseResponse
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

الخطوة 7: بناء صفحات المصادقة

صفحة تسجيل الدخول

// src/app/login/page.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()
 
  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)
 
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })
 
    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }
 
    router.push('/dashboard')
    router.refresh()
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <h2 className="text-3xl font-bold text-center text-gray-900">
          تسجيل الدخول إلى لوحة المهام
        </h2>
 
        <form onSubmit={handleLogin} className="space-y-6">
          {error && (
            <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
              {error}
            </div>
          )}
 
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              البريد الإلكتروني
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              كلمة المرور
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <button
            type="submit"
            disabled={loading}
            className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? 'جارٍ تسجيل الدخول...' : 'تسجيل الدخول'}
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          ليس لديك حساب؟{' '}
          <a href="/signup" className="text-blue-600 hover:underline">
            إنشاء حساب
          </a>
        </p>
      </div>
    </div>
  )
}

صفحة التسجيل

// src/app/signup/page.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
 
export default function SignupPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()
 
  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)
 
    const { error } = await supabase.auth.signUp({
      email,
      password,
    })
 
    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }
 
    router.push('/dashboard')
    router.refresh()
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <h2 className="text-3xl font-bold text-center text-gray-900">
          إنشاء حسابك
        </h2>
 
        <form onSubmit={handleSignup} className="space-y-6">
          {error && (
            <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
              {error}
            </div>
          )}
 
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              البريد الإلكتروني
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              كلمة المرور (6 أحرف كحد أدنى)
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              minLength={6}
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
            />
          </div>
 
          <button
            type="submit"
            disabled={loading}
            className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? 'جارٍ الإنشاء...' : 'إنشاء حساب'}
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          لديك حساب بالفعل؟{' '}
          <a href="/login" className="text-blue-600 hover:underline">
            تسجيل الدخول
          </a>
        </p>
      </div>
    </div>
  )
}

الخطوة 8: بناء لوحة التحكم مع Server Components

تقوم لوحة التحكم بتحميل المهام من جانب الخادم لعرض أولي سريع، ثم تشترك في التغييرات الفورية من جانب العميل.

تخطيط لوحة التحكم

// src/app/dashboard/layout.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) redirect('/login')
 
  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-white shadow-sm border-b">
        <div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
          <h1 className="text-xl font-bold text-gray-900">لوحة المهام</h1>
          <div className="flex items-center gap-4">
            <span className="text-sm text-gray-600">{user.email}</span>
            <form action="/auth/signout" method="post">
              <button
                type="submit"
                className="text-sm text-red-600 hover:underline"
              >
                تسجيل الخروج
              </button>
            </form>
          </div>
        </div>
      </header>
      <main className="max-w-7xl mx-auto px-4 py-8">{children}</main>
    </div>
  )
}

Server Component: تحميل المهام الأولية

// src/app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { TaskBoard } from './task-board'
 
export default async function DashboardPage() {
  const supabase = await createClient()
 
  const { data: tasks, error } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false })
 
  if (error) {
    return <div className="text-red-600">فشل تحميل المهام: {error.message}</div>
  }
 
  return <TaskBoard initialTasks={tasks ?? []} />
}

الخطوة 9: إنشاء مكوّن لوحة المهام الفوري

هذا هو قلب التطبيق — مكوّن عميل يعرض أعمدة Kanban ويشترك في التغييرات الفورية:

// src/app/dashboard/task-board.tsx
'use client'
 
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
 
type Task = {
  id: string
  title: string
  description: string | null
  status: 'todo' | 'in_progress' | 'done'
  created_at: string
  updated_at: string
  user_id: string
}
 
const STATUS_COLUMNS = [
  { key: 'todo', label: 'للتنفيذ', color: 'bg-yellow-100 border-yellow-300' },
  { key: 'in_progress', label: 'قيد التنفيذ', color: 'bg-blue-100 border-blue-300' },
  { key: 'done', label: 'مكتمل', color: 'bg-green-100 border-green-300' },
] as const
 
export function TaskBoard({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState<Task[]>(initialTasks)
  const [newTitle, setNewTitle] = useState('')
  const supabase = createClient()
 
  // الاشتراك في التغييرات الفورية
  useEffect(() => {
    const channel = supabase
      .channel('tasks:updates', { config: { private: true } })
      .on('broadcast', { event: 'INSERT' }, (payload) => {
        const newTask = payload.payload.record as Task
        setTasks((prev) => [newTask, ...prev])
      })
      .on('broadcast', { event: 'UPDATE' }, (payload) => {
        const updated = payload.payload.record as Task
        setTasks((prev) =>
          prev.map((t) => (t.id === updated.id ? updated : t))
        )
      })
      .on('broadcast', { event: 'DELETE' }, (payload) => {
        const deleted = payload.payload.old_record as Task
        setTasks((prev) => prev.filter((t) => t.id !== deleted.id))
      })
      .subscribe()
 
    return () => {
      supabase.removeChannel(channel)
    }
  }, [supabase])
 
  const addTask = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!newTitle.trim()) return
 
    const { data: { user } } = await supabase.auth.getUser()
    if (!user) return
 
    const { error } = await supabase.from('tasks').insert({
      title: newTitle.trim(),
      user_id: user.id,
      status: 'todo',
    })
 
    if (!error) setNewTitle('')
  }
 
  const updateStatus = async (taskId: string, status: Task['status']) => {
    await supabase.from('tasks').update({ status }).eq('id', taskId)
  }
 
  const deleteTask = async (taskId: string) => {
    await supabase.from('tasks').delete().eq('id', taskId)
  }
 
  return (
    <div className="space-y-6">
      {/* نموذج إضافة مهمة */}
      <form onSubmit={addTask} className="flex gap-3">
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="أضف مهمة جديدة..."
          className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          type="submit"
          className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
        >
          إضافة
        </button>
      </form>
 
      {/* أعمدة Kanban */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {STATUS_COLUMNS.map((col) => (
          <div key={col.key} className={`rounded-xl border-2 p-4 ${col.color}`}>
            <h3 className="font-semibold text-lg mb-4">
              {col.label}
              <span className="ml-2 text-sm font-normal text-gray-500">
                ({tasks.filter((t) => t.status === col.key).length})
              </span>
            </h3>
 
            <div className="space-y-3">
              {tasks
                .filter((t) => t.status === col.key)
                .map((task) => (
                  <div
                    key={task.id}
                    className="bg-white rounded-lg p-4 shadow-sm border"
                  >
                    <h4 className="font-medium text-gray-900">{task.title}</h4>
                    {task.description && (
                      <p className="text-sm text-gray-500 mt-1">
                        {task.description}
                      </p>
                    )}
 
                    <div className="mt-3 flex gap-2 flex-wrap">
                      {col.key !== 'todo' && (
                        <button
                          onClick={() =>
                            updateStatus(
                              task.id,
                              col.key === 'done' ? 'in_progress' : 'todo'
                            )
                          }
                          className="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200"
                        >
                          ← رجوع
                        </button>
                      )}
                      {col.key !== 'done' && (
                        <button
                          onClick={() =>
                            updateStatus(
                              task.id,
                              col.key === 'todo' ? 'in_progress' : 'done'
                            )
                          }
                          className="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200 text-blue-700"
                        >
                          تقدّم →
                        </button>
                      )}
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="text-xs px-2 py-1 bg-red-100 rounded hover:bg-red-200 text-red-700"
                      >
                        حذف
                      </button>
                    </div>
                  </div>
                ))}
 
              {tasks.filter((t) => t.status === col.key).length === 0 && (
                <p className="text-sm text-gray-400 text-center py-4">
                  لا توجد مهام
                </p>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

الخطوة 10: إضافة مسار تسجيل الخروج

// src/app/auth/signout/route.ts
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
 
export async function POST() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/login')
}

الخطوة 11: معالج callback المصادقة

يرسل Supabase المستخدمين إلى عنوان callback بعد تأكيد البريد الإلكتروني. أضف هذا المسار:

// src/app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
 
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
 
  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}/dashboard`)
    }
  }
 
  return NextResponse.redirect(`${origin}/login?error=auth`)
}

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

  1. شغّل خادم التطوير:
npm run dev
  1. سجّل على http://localhost:3000/signup ببريد إلكتروني تجريبي
  2. أنشئ مهامًا وانقلها بين الأعمدة
  3. افتح تبويبًا ثانيًا — ستظهر التغييرات فوريًا
  4. تحقق من RLS — سجّل الدخول بحساب مختلف للتأكد من عزل المهام

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

المشاكل الشائعة وحلولها

"relation 'tasks' does not exist" — تأكد من تنفيذ SQL الخطوة 2 على مشروع Supabase الخاص بك.

الكوكيز لا تُحفظ — تحقق من أن الـ middleware مُهيّأ بنمط matcher الصحيح وأن @supabase/ssr مثبّت.

الأحداث الفورية لا تصل — تحقق من إنشاء trigger البث في الخطوة 3. راجع لوحة تحكم Supabase تحت Realtime > Inspector لمعرفة ما إذا كانت الأحداث تتدفق.

أخطاء "Unauthorized" — Row Level Security مفعّل. تأكد من أن المستخدم مصادق عليه قبل إجراء الاستعلامات. تحقق من أن سياسات RLS تربط auth.uid() بالعمود الصحيح.

فقدان الجلسة عند تحديث الصفحة — يجب أن يتعامل الـ middleware مع تحديث الرموز تلقائيًا. إذا فُقدت الجلسات، تحقق من أن setAll في عميل الخادم يكتب الكوكيز بشكل صحيح.

هيكل المشروع

src/
├── app/
│   ├── auth/
│   │   ├── callback/route.ts    # Callback المصادقة
│   │   └── signout/route.ts     # معالج تسجيل الخروج
│   ├── dashboard/
│   │   ├── layout.tsx           # حماية المصادقة + العنوان
│   │   ├── page.tsx             # Server Component (تحميل البيانات)
│   │   └── task-board.tsx       # Client Component (فوري)
│   ├── login/page.tsx           # نموذج تسجيل الدخول
│   └── signup/page.tsx          # نموذج التسجيل
├── lib/
│   └── supabase/
│       ├── client.ts            # عميل المتصفح
│       └── server.ts            # عميل الخادم
└── middleware.ts                 # تحديث الجلسة + حراسة المسارات

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

الآن بعد أن لديك لوحة مهام فورية تعمل، إليك طرق لتوسيعها:

  • إضافة مزودي OAuth — فعّل تسجيل الدخول عبر Google أو GitHub أو Discord من إعدادات Supabase Auth
  • تنفيذ السحب والإفلات — استخدم @dnd-kit/core لتفاعلات Kanban سلسة
  • إضافة Supabase Storage — اسمح للمستخدمين بإرفاق ملفات بالمهام
  • النشر على Vercel — هيّئ متغيرات البيئة وانشر باستخدام vercel deploy
  • إضافة الحضور — اعرض المستخدمين المتصلين حاليًا باستخدام Supabase Realtime Presence

الخلاصة

لقد بنيت لوحة مهام تعاونية full-stack فورية باستخدام Supabase و Next.js 15. الأنماط الأساسية التي تعلمتها هي:

  • إعداد عميل مزدوج — عملاء Supabase منفصلين للمتصفح والخادم مع App Router
  • إدارة الجلسات عبر Middleware — تحديث تلقائي للرموز مع كل طلب
  • Row Level Security — التحكم في الوصول على مستوى قاعدة البيانات دون كتابة كود backend
  • Triggers البث — النهج القابل للتوسع لإشعارات التغييرات الفورية
  • Server Components + Client Components — تحميل البيانات من الخادم مع تفاعلية العميل

تنطبق هذه الأنماط على أي مشروع Supabase + Next.js، سواء كنت تبني تطبيق محادثة أو لوحة تحكم أو منتج SaaS. يتعامل Supabase مع البنية التحتية الثقيلة لتتمكن من التركيز على منطق تطبيقك.