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

أصبح 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`)
}اختبار التطبيق
- شغّل خادم التطوير:
npm run dev- سجّل على
http://localhost:3000/signupببريد إلكتروني تجريبي - أنشئ مهامًا وانقلها بين الأعمدة
- افتح تبويبًا ثانيًا — ستظهر التغييرات فوريًا
- تحقق من 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 مع البنية التحتية الثقيلة لتتمكن من التركيز على منطق تطبيقك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار
تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

بناء مشروع SaaS متكامل باستخدام Next.js 15 و Stripe و Auth.js v5
تعلم كيفية بناء تطبيق SaaS جاهز للإنتاج باستخدام Next.js 15 مع نظام اشتراكات Stripe ومصادقة Auth.js v5. يغطي هذا الدليل التفصيلي إعداد المشروع وتسجيل الدخول عبر OAuth وخطط الأسعار ومعالجة Webhooks والمسارات المحمية.