دليل متكامل لبناء تطبيقات الويب المتكاملة باستخدام Next.js وSupabase
مقدمة: لماذا يجمع المطورون بين Next.js وSupabase؟
أصبح بناء التطبيقات المتكاملة أسرع وأكثر مرونة بفضل الأدوات الحديثة التي تختصر وقت إعداد الواجهة الخلفية وتمنح المطور تجربة تطوير سلسة. ويُعد Supabase واحداً من أبرز هذه الحلول، إذ يقدّم بديلاً مفتوح المصدر لـ Firebase مع اعتماد قوي على قاعدة بيانات Postgres، إلى جانب المصادقة، وواجهة API، والدعم الفوري للبيانات، وتخزين الملفات.
عند دمج Supabase مع Next.js وReact وTailwind CSS، يصبح بإمكانك إنشاء تطبيق متكامل يجمع بين الأداء الجيد، وسهولة التطوير، والتحكم الدقيق في البيانات والصلاحيات. في هذا الدليل سنعيد بناء الفكرة الأساسية لمشروع تدوين متعدد المستخدمين، مع شرح عملي للعناصر التي تحتاجها معظم التطبيقات الحديثة.

ما هو Supabase وما الذي يميّزه؟
Supabase منصة مفتوحة المصدر تساعدك على إنشاء واجهة خلفية جاهزة بسرعة كبيرة. بمجرد إنشاء مشروع جديد، تحصل تلقائياً على:
- قاعدة بيانات
Postgres SQL. - نظام مصادقة للمستخدمين.
- واجهة
APIللتعامل مع البيانات. - إمكانية تفعيل الاشتراكات الفورية
Realtime. - خيارات لتخزين الملفات.
الميزة الجوهرية هنا أن المنصة لا تعتمد على طبقة مبسطة تخفي إمكانات قاعدة البيانات، بل تستفيد مباشرة من قوة Postgres. وهذا يمنحك مرونة عالية في الاستعلامات، وربط الجداول، وفرض سياسات الأمان الدقيقة.
لماذا يُعد Supabase خياراً ممتازاً لتطبيقات Full Stack؟
1. قوة الاستعلامات والاعتماد على SQL
أحد أكبر التحديات في بعض الخدمات الخلفية الجاهزة هو محدودية أساليب الاستعلام. أما في Supabase، فبما أنه مبني فوق Postgres، يمكنك تنفيذ استعلامات مرنة وعالية الكفاءة دون الحاجة إلى كتابة طبقة خلفية مخصصة لكل عملية.
كما أن مكتبة العميل SDK تتيح لك استخدام المرشحات والمعدلات بسهولة لإنشاء أنماط وصول متعددة للبيانات، وهو ما يفيد في التطبيقات الواقعية التي تتطلب أكثر من مجرد عمليات CRUD الأساسية.
2. الصلاحيات والتحكم الدقيق في الوصول
التطبيقات الحقيقية لا تكتفي بإضافة البيانات وعرضها، بل تحتاج إلى قواعد واضحة تحدد من يستطيع القراءة أو الإضافة أو التعديل أو الحذف. هنا تظهر أهمية ميزة Row Level Security في Postgres، والتي يسهّل Supabase تفعيلها وإدارتها.
هذا النوع من التحكم يمنحك حماية على مستوى الصفوف داخل الجدول نفسه، ما يجعل التطبيق أكثر أماناً وقابلية للتوسع.
3. مكونات واجهة جاهزة للمصادقة
توفر مكتبة Supabase UI مكونات جاهزة تساعدك على إطلاق صفحات تسجيل الدخول وإنشاء الحسابات بسرعة. بدلاً من بناء كل شيء من الصفر، يمكنك الاستفادة من مكوّن Auth لربط واجهتك مباشرة بخدمات المصادقة.
4. دعم عدة مزودي تسجيل دخول
يدعم Supabase مجموعة واسعة من أساليب المصادقة، مثل:
- البريد الإلكتروني وكلمة المرور.
- الرابط السحري
Magic Link. Google.Facebook.Apple.GitHub.Twitter.Azure.GitLab.Bitbucket.
5. مشروع مفتوح المصدر بالكامل
من أبرز مزايا Supabase أنه مفتوح المصدر، بما في ذلك الخدمات الخلفية نفسها. وهذا يمنحك خيارين مهمين:
- استخدام النسخة المستضافة كخدمة مُدارة.
- استضافة المشروع ذاتياً باستخدام
DockerعلىAWSأوGCPأوAzure.
وهذا يقلل من مشكلة الارتباط بمزوّد واحد للخدمة على المدى الطويل.
فكرة المشروع التطبيقي
سنفترض في هذا الدليل إنشاء تطبيق تدوين متعدد المستخدمين. هذا النوع من المشاريع مناسب جداً لأنه يجمع أغلب احتياجات التطبيقات الحديثة في مثال واحد، مثل:
- التنقل بين الصفحات
Routing. - قاعدة البيانات.
- المصادقة وتفويض الصلاحيات.
- إنشاء المقالات وعرضها وتعديلها وحذفها.
- التحديثات الفورية.
- التحكم الدقيق في الوصول.
إعداد مشروع Next.js
ابدأ بإنشاء التطبيق باستخدام الأمر التالي:
npx create-next-app next-supabase
بعد ذلك انتقل إلى مجلد المشروع وثبّت الحزم المطلوبة:
npm install @supabase/supabase-js @supabase/ui react-simplemde-editor easymde react-markdown uuid
npm install tailwindcss@latest @tailwindcss/typography postcss@latest autoprefixer@latest
ثم أنشئ ملفات إعداد Tailwind CSS:
npx tailwindcss init -p
حدّث ملف tailwind.config.js لإضافة إضافة التنسيق الخاصة بالمحتوى:
plugins: [ require('@tailwindcss/typography') ]
واستبدل محتوى ملف styles/globals.css بما يلي:
@tailwind base;
@tailwind components;
@tailwind utilities;
إنشاء مشروع Supabase
بعد تجهيز المشروع محلياً، توجه إلى موقع Supabase وأنشئ مشروعاً جديداً. يمكنك تسجيل الدخول عبر GitHub ثم إنشاء مشروع ضمن المؤسسة المتاحة في حسابك.

أدخل اسم المشروع وكلمة المرور، ثم انتظر بضع دقائق حتى يتم تجهيز البيئة الخلفية.
إنشاء جدول قاعدة البيانات وتفعيل سياسات الأمان
من لوحة التحكم، افتح محرر SQL ثم نفّذ الاستعلام التالي لإنشاء جدول المقالات:

CREATE TABLE posts (
id bigint generated by default as identity primary key,
user_id uuid references auth.users not null,
user_email text,
title text,
content text,
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table posts enable row level security;
create policy "Individuals can create posts."
on posts for insert
with check (auth.uid() = user_id);
create policy "Individuals can update their own posts."
on posts for update
using (auth.uid() = user_id);
create policy "Individuals can delete their own posts."
on posts for delete
using (auth.uid() = user_id);
create policy "Posts are public."
on posts for select
using (true);
هذا الاستعلام ينشئ جدول posts ويطبّق سياسات أمان مهمة، منها:
- إتاحة قراءة المقالات للجميع.
- السماح بإنشاء المقالات فقط للمستخدمين المسجلين.
- السماح بالتعديل أو الحذف فقط لصاحب المقال.
بعد التنفيذ، يمكنك التحقق من إنشاء الجدول عبر Table editor.

ربط Next.js مع Supabase باستخدام متغيرات البيئة
لكي يتمكن التطبيق من الوصول إلى خدمات الواجهة الخلفية، أنشئ ملفاً باسم .env.local في جذر المشروع، ثم أضف القيم التالية:
NEXT_PUBLIC_SUPABASE_URL=https://app-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-public-api-key
تذكّر أن أي متغير تريد إتاحته للمتصفح في Next.js يجب أن يبدأ بالبادئة NEXT_PUBLIC_.
يمكنك العثور على رابط API URL والمفتاح العام API Key من إعدادات المشروع في لوحة Supabase.

بعدها أنشئ ملف api.js في جذر المشروع:
// api.js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
بهذا أصبح كائن supabase متاحاً للاستيراد في أي جزء من التطبيق.
أمثلة أساسية للتعامل مع البيانات والمصادقة
جلب البيانات من الجدول
import { supabase } from '../path/to/api'
const { data, error } = await supabase
.from('posts')
.select()
إضافة سجل جديد
const { data, error } = await supabase
.from('posts')
.insert([
{
title: "Hello World",
content: "My first post",
user_id: "some-user-id",
user_email: "myemail@gmail.com"
}
])
إنشاء حساب جديد
const { user, session, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
تسجيل الدخول
const { user, session, error } = await supabase.auth.signIn({
email: 'example@email.com',
password: 'example-password',
})
في التطبيق العملي سنستخدم مكون Auth الجاهز بدلاً من كتابة منطق المصادقة يدوياً.
إعداد الهيكل العام للتطبيق والتنقل
الخطوة التالية هي تحديث ملف pages/_app.js لإضافة شريط تنقل، والتحقق من حالة تسجيل دخول المستخدم، وإظهار روابط مخصصة مثل إنشاء مقال أو إدارة المقالات الخاصة به.
// pages/_app.js
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { supabase } from '../api'
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
const [user, setUser] = useState(null);
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(async () => checkUser())
checkUser()
return () => {
authListener?.unsubscribe()
};
}, [])
async function checkUser() {
const user = supabase.auth.user()
setUser(user)
}
return (
<div>
<nav className="p-6 border-b border-gray-300">
<Link href="/">
<span className="mr-6 cursor-pointer">Home</span>
</Link>
{user && (
<Link href="/create-post">
<span className="mr-6 cursor-pointer">Create Post</span>
</Link>
)}
<Link href="/profile">
<span className="mr-6 cursor-pointer">Profile</span>
</Link>
</nav>
<div className="py-8 px-16">
<Component {...pageProps} />
</div>
</div>
)
}
export default MyApp
الفكرة هنا أن التطبيق يراقب أي تغيّر في حالة المصادقة عبر onAuthStateChange، ثم يحدّث الواجهة تبعاً لذلك.
إنشاء صفحة الملف الشخصي وتسجيل الدخول
لإنشاء صفحة المصادقة، أضف الملف pages/profile.js:
// pages/profile.js
import { Auth, Typography, Button } from "@supabase/ui";
const { Text } = Typography
import { supabase } from '../api'
function Profile(props) {
const { user } = Auth.useUser();
if (user) return (
<>
<Text>Signed in: {user.email}</Text>
<Button block onClick={() => props.supabaseClient.auth.signOut()}>Sign out</Button>
</>
);
return props.children
}
export default function AuthProfile() {
return (
<Auth.UserContextProvider supabaseClient={supabase}>
<Profile supabaseClient={supabase}>
<Auth supabaseClient={supabase} />
</Profile>
</Auth.UserContextProvider>
)
}
يعرض هذا المكوّن نموذج تسجيل الدخول أو إنشاء الحساب للمستخدم غير المسجل، بينما يعرض بريد المستخدم وزر تسجيل الخروج عند نجاح المصادقة.
إنشاء صفحة إضافة مقال جديد
لإنشاء المقالات، أضف الملف pages/create-post.js:
// pages/create-post.js
import { useState } from 'react'
import { v4 as uuid } from 'uuid'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import "easymde/dist/easymde.min.css"
import { supabase } from '../api'
const SimpleMDE = dynamic(() => import('react-simplemde-editor'), { ssr: false })
const initialState = { title: '', content: '' }
function CreatePost() {
const [post, setPost] = useState(initialState)
const { title, content } = post
const router = useRouter()
function onChange(e) {
setPost(() => ({ ...post, [e.target.name]: e.target.value }))
}
async function createNewPost() {
if (!title || !content) return
const user = supabase.auth.user()
const id = uuid()
post.id = id
const { data } = await supabase
.from('posts')
.insert([
{ title, content, user_id: user.id, user_email: user.email }
])
.single()
router.push(`/posts/${data.id}`)
}
return (
<div>
<h1 className="text-3xl font-semibold tracking-wide mt-6">Create new post</h1>
<input onChange={onChange} name="title" placeholder="Title" value={post.title} className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2" />
<SimpleMDE value={post.content} onChange={value => setPost({ ...post, content: value })} />
<button type="button" className="mb-4 bg-green-600 text-white font-semibold px-8 py-2 rounded-lg" onClick={createNewPost}>Create Post</button>
</div>
)
}
export default CreatePost
تستخدم هذه الصفحة محرر Markdown لكتابة المحتوى، ثم تُرسل البيانات إلى الجدول باستخدام كائن supabase. وعند تسجيل دخول المستخدم، تُضاف ترويسة الوصول تلقائياً من مكتبة العميل دون الحاجة لتمريرها يدوياً.
عرض مقال واحد باستخدام المسارات الديناميكية
لعرض مقال مفرد، أنشئ الملف pages/posts/[id].js:
// pages/posts/[id].js
import { useRouter } from 'next/router'
import ReactMarkdown from 'react-markdown'
import { supabase } from '../../api'
export default function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h1 className="text-5xl mt-4 font-semibold tracking-wide">{post.title}</h1>
<p className="text-sm font-light my-4">by {post.user_email}</p>
<div className="mt-8">
<ReactMarkdown className='prose' children={post.content} />
</div>
</div>
)
}
export async function getStaticPaths() {
const { data, error } = await supabase
.from('posts')
.select('id')
const paths = data.map(post => ({ params: { id: JSON.stringify(post.id) }}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
const { id } = params
const { data } = await supabase
.from('posts')
.select()
.filter('id', 'eq', id)
.single()
return { props: { post: data } }
}
تعتمد الصفحة على getStaticPaths لإنشاء المسارات ديناميكياً وقت البناء، وعلى getStaticProps لجلب بيانات المقال مسبقاً، وهو ما يحسن الأداء وتجربة القراءة.
جلب قائمة المقالات وعرضها في الصفحة الرئيسية
لتحديث الصفحة الرئيسية، استخدم الملف pages/index.js التالي:
// pages/index.js
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../api'
export default function Home() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchPosts()
}, [])
async function fetchPosts() {
const { data, error } = await supabase
.from('posts')
.select()
setPosts(data)
setLoading(false)
}
if (loading) return <p className="text-2xl">Loading ...</p>
if (!posts.length) return <p className="text-2xl">No posts.</p>
return (
<div>
<h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2">Posts</h1>
{posts.map(post => (
<Link key={post.id} href={`/posts/${post.id}`}>
<div className="cursor-pointer border-b border-gray-300 mt-8 pb-4">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-500 mt-2">Author: {post.user_email}</p>
</div>
</Link>
))}
</div>
)
}
بهذا ستظهر جميع المقالات المتاحة مع إمكانية الانتقال إلى صفحة كل مقال على حدة.
تشغيل التطبيق محلياً واختباره
بعد اكتمال الخطوات السابقة، شغّل الخادم المحلي بالأمر التالي:
npm run dev
عند فتح التطبيق، ستظهر الواجهة الرئيسية، ويمكنك الانتقال إلى صفحة Profile لإنشاء حساب جديد أو تسجيل الدخول.

بعد التحقق من البريد الإلكتروني أو استخدام الرابط السحري، ستتمكن من إنشاء مقال جديد.

كما ستظهر المقالات في الصفحة الرئيسية مع إمكانية فتح كل مقال.

إدارة مقالات المستخدم: عرض وتعديل وحذف
إنشاء صفحة My Posts
لعرض المقالات الخاصة بالمستخدم الحالي فقط، أنشئ الملف pages/my-posts.js:
// pages/my-posts.js
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../api'
export default function MyPosts() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetchPosts()
}, [])
async function fetchPosts() {
const user = supabase.auth.user()
const { data } = await supabase
.from('posts')
.select('*')
.filter('user_id', 'eq', user.id)
setPosts(data)
}
async function deletePost(id) {
await supabase
.from('posts')
.delete()
.match({ id })
fetchPosts()
}
return (
<div>
<h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2">My Posts</h1>
{posts.map((post, index) => (
<div key={index} className="border-b border-gray-300 mt-8 pb-4">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-500 mt-2 mb-2">Author: {post.user_email}</p>
<Link href={`/edit-post/${post.id}`}>
<a className="text-sm mr-4 text-blue-500">Edit Post</a>
</Link>
<Link href={`/posts/${post.id}`}>
<a className="text-sm mr-4 text-blue-500">View Post</a>
</Link>
<button className="text-sm mr-4 text-red-500" onClick={() => deletePost(post.id)}>Delete Post</button>
</div>
))}
</div>
)
}
هنا نستخدم user.id لتصفية النتائج وإرجاع المقالات الخاصة بالمستخدم فقط.
إنشاء صفحة تعديل المقال
أنشئ الملف pages/edit-post/[id].js:
// pages/edit-post/[id].js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import "easymde/dist/easymde.min.css"
import { supabase } from '../../api'
const SimpleMDE = dynamic(() => import('react-simplemde-editor'), { ssr: false })
function EditPost() {
const [post, setPost] = useState(null)
const router = useRouter()
const { id } = router.query
useEffect(() => {
fetchPost()
async function fetchPost() {
if (!id) return
const { data } = await supabase
.from('posts')
.select()
.filter('id', 'eq', id)
.single()
setPost(data)
}
}, [id])
if (!post) return null
function onChange(e) {
setPost(() => ({ ...post, [e.target.name]: e.target.value }))
}
const { title, content } = post
async function updateCurrentPost() {
if (!title || !content) return
await supabase
.from('posts')
.update([{ title, content }])
.match({ id })
router.push('/my-posts')
}
return (
<div>
<h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2">Edit post</h1>
<input onChange={onChange} name="title" placeholder="Title" value={post.title} className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2" />
<SimpleMDE value={post.content} onChange={value => setPost({ ...post, content: value })} />
<button className="mb-4 bg-blue-600 text-white font-semibold px-8 py-2 rounded-lg" onClick={updateCurrentPost}>Update Post</button>
</div>
)
}
export default EditPost
بعد ذلك أضف رابطاً جديداً في ملف pages/_app.js لإظهار صفحة المقالات الخاصة بالمستخدم:
{user && (
<Link href="/my-posts">
<span className="mr-6 cursor-pointer">My Posts</span>
</Link>
)}
تفعيل التحديثات الفورية Realtime
من أقوى مزايا Supabase سهولة تفعيل التحديثات الفورية. لتفعيلها:
- افتح لوحة المشروع.
- انتقل إلى
Databases. - ثم
Replication. - فعّل
Realtimeلجدولposts.
بعدها حدّث useEffect في الصفحة الرئيسية ليصبح كالتالي:
useEffect(() => {
fetchPosts()
const mySubscription = supabase
.from('posts')
.on('*', () => fetchPosts())
.subscribe()
return () => supabase.removeSubscription(mySubscription)
}, [])
بهذا سيتم تحديث قائمة المقالات تلقائياً عند أي عملية إضافة أو تعديل أو حذف.
أفضل ممارسات لتحسين SEO وقبول AdSense في هذا النوع من المقالات
- اكتب محتوى أصلياً يشرح الفكرة بلغة واضحة ويضيف أمثلة عملية.
- استخدم عناوين فرعية منطقية تتضمن الكلمات المفتاحية بشكل طبيعي.
- قلّل من التكرار، وركّز على الإجابة عن أسئلة المستخدم الفعلية.
- أضف صوراً توضيحية بنصوص
altدقيقة ومحسّنة لمحركات البحث. - حافظ على تنسيق الأكواد بوضوح داخل وسوم
preوcode. - اجعل المقال قابلاً للقراءة على الهاتف من خلال بنية بسيطة وفقرة قصيرة وقوائم واضحة.
الخلاصة التقنية
يُعد الجمع بين Next.js وSupabase خياراً عملياً جداً لبناء تطبيقات Full Stack حديثة دون التعقيد التقليدي المرتبط بإعداد واجهة خلفية كاملة من الصفر. القوة الحقيقية هنا لا تكمن فقط في سرعة البدء، بل في التوازن بين سهولة الاستخدام ومرونة Postgres وسياسات الأمان الدقيقة. إذا كنت تبحث عن بنية حديثة لتطوير تطبيقات قابلة للتوسع مع تجربة مطور ممتازة، فهذه المنظومة تستحق التجربة بجدية.