بناء تطبيق متكامل باستخدام Appwrite Cloud و Next.js 15

Appwrite هو منصة خلفية مفتوحة المصدر (BaaS) توفر المصادقة وقواعد البيانات وتخزين الملفات والوظائف السحابية وإمكانيات الوقت الفعلي بشكل جاهز. على عكس البدائل المغلقة المصدر، يمنحك Appwrite التحكم الكامل — يمكنك استضافته ذاتياً أو استخدام Appwrite Cloud للحصول على تجربة مُدارة. بدمجه مع Next.js 15 و App Router، تحصل على بنية تطبيق حديثة وآمنة الأنواع تتعامل مع العرض من جهة الخادم والتفاعل من جهة العميل.
في هذا الدليل التعليمي، ستبني مدير إشارات مرجعية — تطبيق متكامل يمكن للمستخدمين من خلاله حفظ وتنظيم ووسم ومشاركة الإشارات المرجعية. ستنفّذ المصادقة عبر OAuth وقاعدة بيانات مستندات لتخزين الإشارات وتخزين ملفات للقطات الشاشة وتحديثات فورية عند إضافة أو تعديل الإشارات.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- حساب Appwrite Cloud — المستوى المجاني متاح على cloud.appwrite.io
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router (الصفحات، التخطيطات، مكونات الخادم)
- محرر أكواد (يُفضّل VS Code)
ما ستبنيه
تطبيق مدير إشارات مرجعية بالميزات التالية:
- مصادقة OAuth (Google وGitHub) مع إدارة الجلسات
- قاعدة بيانات مستندات لتخزين الإشارات مع المجموعات والسمات
- تخزين ملفات لصور الإشارات المرجعية والأيقونات
- اشتراكات فورية للتحديثات المباشرة عبر التبويبات
- مكونات الخادم لتحميل البيانات الأولية المحسّنة لمحركات البحث
- إجراءات الخادم للعمليات الآمنة
- تنظيم بالوسوم مع التصفية والبحث
الخطوة 1: إنشاء مشروع Appwrite Cloud
توجّه إلى cloud.appwrite.io وأنشئ مشروعاً جديداً:
- انقر على Create Project
- أدخل اسماً مثل "Bookmark Manager"
- اختر المنطقة المفضلة
- بمجرد الإنشاء، سجّل معرّف المشروع من إعدادات المشروع
بعد ذلك، أعدّ المنصة. اذهب إلى Overview وأضف منصة Web:
- الاسم: Bookmark Manager Web
- اسم المضيف:
localhost(للتطوير)
هذا يسجّل تطبيق الويب ويسمح له بالتواصل مع واجهات Appwrite.
الخطوة 2: إعداد مشروع Next.js
أنشئ تطبيق Next.js 15 جديد مع TypeScript و Tailwind CSS:
npx create-next-app@latest bookmark-manager --typescript --tailwind --app --src-dir --eslint
cd bookmark-managerثبّت حزمة Appwrite SDK:
npm install appwrite node-appwriteحزمة appwrite للاستخدام من جهة العميل، بينما node-appwrite هي SDK للخادم لاستخدامها في مكونات الخادم وإجراءات الخادم.
الخطوة 3: إعداد متغيرات البيئة
أنشئ ملف .env.local في جذر المشروع:
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=your-project-id
APPWRITE_API_KEY=your-api-keyالبادئة NEXT_PUBLIC_ تجعل المتغيرات متاحة على جانب العميل. أما APPWRITE_API_KEY فهو للخادم فقط ولا يجب كشفه للمتصفح.
لإنشاء مفتاح API، اذهب إلى لوحة Appwrite، وانتقل إلى Overview > API Keys، وأنشئ مفتاحاً بالصلاحيات التالية:
databases.readوdatabases.writecollections.readوcollections.writedocuments.readوdocuments.writefiles.readوfiles.writeusers.read
الخطوة 4: إنشاء إعدادات Appwrite SDK
أنشئ إعدادَي SDK — واحد للعميل وآخر للخادم.
SDK العميل
// src/lib/appwrite/client.ts
import { Client, Account, Databases, Storage } from "appwrite";
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);
export const account = new Account(client);
export const databases = new Databases(client);
export const storage = new Storage(client);
export { client };SDK الخادم
// src/lib/appwrite/server.ts
import { Client, Databases, Storage, Users } from "node-appwrite";
export function createAdminClient() {
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
.setKey(process.env.APPWRITE_API_KEY!);
return {
databases: new Databases(client),
storage: new Storage(client),
users: new Users(client),
};
}
export function createSessionClient(session: string) {
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)
.setSession(session);
return {
account: new Account(client),
databases: new Databases(client),
};
}يستخدم createAdminClient مفتاح API للوصول الكامل، بينما createSessionClient يستخدم جلسة المستخدم للطلبات المصادق عليها التي تحترم الصلاحيات.
الخطوة 5: إعداد مخطط قاعدة البيانات
في لوحة Appwrite، انتقل إلى Databases وأنشئ قاعدة بيانات جديدة باسم bookmark_db. ثم أنشئ المجموعات التالية:
مجموعة الإشارات المرجعية
أنشئ مجموعة باسم bookmarks بالسمات التالية:
| السمة | النوع | مطلوب | الوصف |
|---|---|---|---|
url | String (2048) | نعم | رابط الإشارة |
title | String (256) | نعم | عنوان الصفحة |
description | String (1024) | لا | وصف الصفحة |
tags | String[] (50) | لا | مصفوفة الوسوم |
thumbnailId | String (36) | لا | معرّف ملف الصورة المصغرة |
favicon | String (2048) | لا | رابط الأيقونة |
userId | String (36) | نعم | معرّف المستخدم المالك |
isPublic | Boolean | نعم | هل الإشارة عامة |
createdAt | DateTime | نعم | تاريخ الإنشاء |
أنشئ الفهارس للاستعلام الفعال:
- فهرس على
userId— النوع: Key - فهرس على
tags— النوع: Key (فهرس مصفوفة) - فهرس على
createdAt— النوع: Key، الترتيب: تنازلي
صلاحيات المجموعة
حدد صلاحيات مجموعة bookmarks:
- الجميع — قراءة (للإشارات العامة)
- المستخدمون — إنشاء، قراءة، تعديل، حذف
سنضيف صلاحيات على مستوى المستند للتحكم في الوصول لكل إشارة.
حدد ثوابت معرّفات قاعدة البيانات والمجموعة:
// src/lib/appwrite/config.ts
export const DATABASE_ID = "bookmark_db";
export const BOOKMARKS_COLLECTION_ID = "bookmarks";
export const THUMBNAILS_BUCKET_ID = "thumbnails";الخطوة 6: تنفيذ المصادقة
يدعم Appwrite مصادقة البريد/كلمة المرور و OAuth والهاتف والرابط السحري. سننفّذ مصادقة Google و GitHub عبر OAuth.
إعداد مزودي OAuth
في لوحة Appwrite، اذهب إلى Auth > Settings وفعّل:
- Google — أضف معرّف عميل Google Cloud OAuth 2.0 والسر
- GitHub — أضف معرّف تطبيق GitHub OAuth والسر
حدد رابط إعادة التوجيه إلى http://localhost:3000/auth/callback لكلا المزودَين.
سياق المصادقة
// src/lib/auth/context.tsx
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { account } from "@/lib/appwrite/client";
import { Models } from "appwrite";
type AuthContextType = {
user: Models.User<Models.Preferences> | null;
loading: boolean;
login: (provider: "google" | "github") => void;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkUser();
}, []);
async function checkUser() {
try {
const currentUser = await account.get();
setUser(currentUser);
} catch {
setUser(null);
} finally {
setLoading(false);
}
}
function login(provider: "google" | "github") {
const redirectUrl = `${window.location.origin}/auth/callback`;
const failureUrl = `${window.location.origin}/auth/failure`;
account.createOAuth2Session(provider, redirectUrl, failureUrl);
}
async function logout() {
await account.deleteSession("current");
setUser(null);
}
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
}صفحة الاستجابة للمصادقة
// src/app/auth/callback/page.tsx
import { redirect } from "next/navigation";
export default function AuthCallback() {
redirect("/dashboard");
}صفحة تسجيل الدخول
// src/app/login/page.tsx
"use client";
import { useAuth } from "@/lib/auth/context";
export default function LoginPage() {
const { login, loading, user } = useAuth();
if (loading) return <div className="flex justify-center p-8">جارٍ التحميل...</div>;
if (user) return redirect("/dashboard");
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">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">مدير الإشارات المرجعية</h1>
<p className="mt-2 text-gray-600">سجّل دخولك لإدارة إشاراتك المرجعية</p>
</div>
<div className="space-y-4">
<button
onClick={() => login("google")}
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
<span>المتابعة مع Google</span>
</button>
<button
onClick={() => login("github")}
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition"
>
<span>المتابعة مع GitHub</span>
</button>
</div>
</div>
</div>
);
}الخطوة 7: بناء عمليات CRUD للإشارات
أنشئ طبقة خدمات لعمليات الإشارات باستخدام إجراءات الخادم:
// src/lib/actions/bookmarks.ts
"use server";
import { createAdminClient } from "@/lib/appwrite/server";
import { DATABASE_ID, BOOKMARKS_COLLECTION_ID } from "@/lib/appwrite/config";
import { ID, Query } from "node-appwrite";
import { revalidatePath } from "next/cache";
export type Bookmark = {
$id: string;
url: string;
title: string;
description: string;
tags: string[];
thumbnailId: string | null;
favicon: string;
userId: string;
isPublic: boolean;
createdAt: string;
};
export async function createBookmark(formData: FormData) {
const { databases } = createAdminClient();
const url = formData.get("url") as string;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const tags = (formData.get("tags") as string)
.split(",")
.map((t) => t.trim())
.filter(Boolean);
const userId = formData.get("userId") as string;
const isPublic = formData.get("isPublic") === "true";
const bookmark = await databases.createDocument(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
ID.unique(),
{
url,
title,
description,
tags,
userId,
isPublic,
favicon: `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`,
createdAt: new Date().toISOString(),
}
);
revalidatePath("/dashboard");
return bookmark;
}
export async function getBookmarks(userId: string) {
const { databases } = createAdminClient();
const response = await databases.listDocuments(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
[
Query.equal("userId", userId),
Query.orderDesc("createdAt"),
Query.limit(50),
]
);
return response.documents as unknown as Bookmark[];
}
export async function deleteBookmark(bookmarkId: string) {
const { databases } = createAdminClient();
await databases.deleteDocument(
DATABASE_ID,
BOOKMARKS_COLLECTION_ID,
bookmarkId
);
revalidatePath("/dashboard");
}الخطوة 8: بناء واجهة لوحة التحكم
مكون بطاقة الإشارة
// src/components/BookmarkCard.tsx
"use client";
import { Bookmark, deleteBookmark } from "@/lib/actions/bookmarks";
import { useState } from "react";
export function BookmarkCard({ bookmark }: { bookmark: Bookmark }) {
const [isDeleting, setIsDeleting] = useState(false);
async function handleDelete() {
if (!confirm("هل تريد حذف هذه الإشارة؟")) return;
setIsDeleting(true);
await deleteBookmark(bookmark.$id);
}
return (
<div className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition group">
<div className="flex items-start gap-3">
<img src={bookmark.favicon} alt="" className="w-6 h-6 mt-1 rounded" loading="lazy" />
<div className="flex-1 min-w-0">
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-medium truncate block"
>
{bookmark.title}
</a>
{bookmark.description && (
<p className="text-sm text-gray-500 mt-1 line-clamp-2">{bookmark.description}</p>
)}
<div className="flex items-center gap-2 mt-2">
{bookmark.tags.map((tag) => (
<span key={tag} className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-700">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
);
}نموذج إضافة إشارة
// src/components/AddBookmarkForm.tsx
"use client";
import { createBookmark } from "@/lib/actions/bookmarks";
import { useAuth } from "@/lib/auth/context";
import { useState } from "react";
export function AddBookmarkForm() {
const { user } = useAuth();
const [isOpen, setIsOpen] = useState(false);
if (!user) return null;
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
+ إضافة إشارة
</button>
{isOpen && (
<form
action={async (formData) => {
formData.set("userId", user.$id);
await createBookmark(formData);
setIsOpen(false);
}}
className="mt-4 bg-white rounded-lg border border-gray-200 p-6 space-y-4"
>
<div>
<label className="block text-sm font-medium text-gray-700">الرابط</label>
<input name="url" type="url" required placeholder="https://example.com" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">العنوان</label>
<input name="title" required placeholder="عنوان الصفحة" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">الوصف</label>
<textarea name="description" rows={2} placeholder="وصف مختصر (اختياري)" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">الوسوم</label>
<input name="tags" placeholder="react, nextjs, tutorial (مفصولة بفواصل)" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
</div>
<div className="flex items-center gap-2">
<input name="isPublic" type="checkbox" value="true" id="isPublic" />
<label htmlFor="isPublic" className="text-sm text-gray-700">جعل الإشارة عامة</label>
</div>
<div className="flex gap-3">
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">حفظ</button>
<button type="button" onClick={() => setIsOpen(false)} className="px-4 py-2 text-gray-600 hover:text-gray-800">إلغاء</button>
</div>
</form>
)}
</div>
);
}الخطوة 9: إضافة تخزين الملفات للصور المصغرة
أنشئ حاوية تخزين في لوحة Appwrite:
- اذهب إلى Storage وأنشئ حاوية باسم
thumbnails - حدد الحجم الأقصى للملف بـ 5 ميجابايت
- اسمح بامتدادات:
jpg,jpeg,png,webp,gif - حدد الصلاحيات: المستخدمون يمكنهم الإنشاء والقراءة
// src/lib/actions/storage.ts
"use server";
import { createAdminClient } from "@/lib/appwrite/server";
import { THUMBNAILS_BUCKET_ID } from "@/lib/appwrite/config";
import { ID } from "node-appwrite";
export async function uploadThumbnail(formData: FormData) {
const { storage } = createAdminClient();
const file = formData.get("file") as File;
if (!file || file.size === 0) return null;
const response = await storage.createFile(
THUMBNAILS_BUCKET_ID,
ID.unique(),
file
);
return response.$id;
}الخطوة 10: إضافة الاشتراكات الفورية
يوفر Appwrite إمكانيات الوقت الفعلي عبر اشتراكات WebSocket. لنضف تحديثات مباشرة عند تغيير الإشارات:
// src/hooks/useRealtimeBookmarks.ts
"use client";
import { useEffect } from "react";
import { client } from "@/lib/appwrite/client";
import { DATABASE_ID, BOOKMARKS_COLLECTION_ID } from "@/lib/appwrite/config";
import { Bookmark } from "@/lib/actions/bookmarks";
export function useRealtimeBookmarks(
userId: string,
onUpdate: (bookmarks: Bookmark[]) => void,
currentBookmarks: Bookmark[]
) {
useEffect(() => {
const channel = `databases.${DATABASE_ID}.collections.${BOOKMARKS_COLLECTION_ID}.documents`;
const unsubscribe = client.subscribe(channel, (response) => {
const { events, payload } = response as any;
if (payload.userId !== userId) return;
let updated = [...currentBookmarks];
if (events.some((e: string) => e.includes(".create"))) {
updated = [payload, ...updated];
} else if (events.some((e: string) => e.includes(".update"))) {
updated = updated.map((b) => (b.$id === payload.$id ? payload : b));
} else if (events.some((e: string) => e.includes(".delete"))) {
updated = updated.filter((b) => b.$id !== payload.$id);
}
onUpdate(updated);
});
return () => unsubscribe();
}, [userId, currentBookmarks, onUpdate]);
}الآن إذا فتحت تبويبَين في المتصفح، فإن إنشاء إشارة في أحدهما سيظهر فوراً في الآخر.
الخطوة 11: إضافة Middleware لحماية المسارات
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("a_session");
const isAuthPage = request.nextUrl.pathname.startsWith("/login");
const isDashboard = request.nextUrl.pathname.startsWith("/dashboard");
if (isDashboard && !session) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (isAuthPage && session) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/login"],
};الخطوة 12: النشر في الإنتاج
النشر على Vercel
- ارفع الكود على GitHub
- استورد المستودع في Vercel
- أضف متغيرات البيئة في لوحة Vercel
- حدّث اسم مضيف منصة Appwrite من
localhostإلى نطاقك الإنتاجي
تحديث منصة Appwrite
في لوحة Appwrite، اذهب إلى Overview وأضف اسم المضيف الإنتاجي. حدّث روابط إعادة توجيه OAuth لتشمل النطاق الإنتاجي.
اختبار التطبيق
- تدفق المصادقة: انقر "المتابعة مع Google" وتحقق من اكتمال التوجيه عبر OAuth
- إنشاء إشارات: أضف بعض الإشارات بوسوم مختلفة وتحقق من ظهورها
- المزامنة الفورية: افتح تبويبَين، أضف إشارة في أحدهما، وتأكد من ظهورها في الآخر
- تصفية الوسوم: انقر على الوسوم لتصفية الإشارات
- حذف الإشارات: مرر فوق إشارة وانقر أيقونة الحذف
استكشاف الأخطاء
خطأ "Missing required attribute"
تأكد من أن جميع السمات المطلوبة في مجموعة Appwrite تتطابق مع البيانات المرسلة. تحقق مرة أخرى من أسماء السمات وأنواعها في اللوحة.
فشل إعادة توجيه OAuth
- تحقق من تطابق رابط إعادة التوجيه بالضبط في كودك وإعدادات مزود OAuth
- تأكد من أن اسم مضيف منصة Appwrite يتضمن النطاق الصحيح
- تحقق من تفعيل مزود OAuth في إعدادات مصادقة Appwrite
عدم إطلاق أحداث الوقت الفعلي
- تأكد من أن صلاحيات المجموعة تسمح بالقراءة
- تحقق من إنشاء اتصال WebSocket (ابحث عن اتصالات
wss://في أدوات المطور) - تأكد من الاشتراك بتنسيق القناة الصحيح
Appwrite مقارنة بمزودي BaaS الآخرين
| الميزة | Appwrite | Supabase | Firebase |
|---|---|---|---|
| مفتوح المصدر | نعم | نعم | لا |
| استضافة ذاتية | Docker | Docker | لا |
| قاعدة البيانات | مستندات (MariaDB) | PostgreSQL | مستندات (Firestore) |
| مزودو المصادقة | أكثر من 30 | أكثر من 20 | أكثر من 20 |
| الوقت الفعلي | WebSocket | WebSocket | WebSocket |
| تخزين الملفات | مدمج | مدمج | مدمج |
| الوظائف السحابية | أوقات تشغيل متعددة | Edge Functions | Cloud Functions |
| المستوى المجاني | سخي | سخي | محدود |
يتميّز Appwrite بنموذج قاعدة البيانات المستندية ودعم مزودي OAuth الواسع وإمكانية استضافة المنصة بالكامل ذاتياً بأمر Docker Compose واحد.
الخطوات التالية
- إضافة وظائف سحابية — أنشئ Appwrite Functions لتوليد معاينات الإشارات تلقائياً
- تنفيذ المجموعات — اسمح للمستخدمين بتنظيم الإشارات في مجلدات
- إضافة إضافة متصفح — ابنِ إضافة Chrome لحفظ الإشارات بنقرة واحدة
- التصدير والاستيراد — دعم تصدير الإشارات كـ JSON واستيرادها من ملفات المتصفح
- المشاركة التعاونية — اسمح للمستخدمين بمشاركة مجموعات الإشارات مع الفرق
الخلاصة
لقد بنيت مدير إشارات مرجعية متكامل باستخدام Appwrite Cloud و Next.js 15. يتضمن التطبيق مصادقة OAuth وقاعدة بيانات مستندات مع تنظيم بالوسوم وتخزين ملفات للصور المصغرة واشتراكات فورية للتحديثات المباشرة. يوفر Appwrite بديلاً مفتوح المصدر قابلاً للاستضافة الذاتية لمنصات BaaS المغلقة، مما يمنحك التحكم الكامل في الخلفية مع الحفاظ على إنتاجية المطور. يُنشئ الجمع بين قواعد بيانات Appwrite المستندية وإجراءات خادم Next.js بنية نظيفة وآمنة الأنواع تتوسع من النموذج الأولي إلى الإنتاج.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15
تعلّم كيفية بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15. يغطي هذا الدليل تصميم المخططات والاستعلامات والتعديلات والاشتراكات الفورية والمصادقة ورفع الملفات — مع أمان أنواع شامل.

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

بناء تطبيق كامل مع Firebase و Next.js 15: المصادقة، Firestore والتحديث الفوري
تعلم كيفية بناء تطبيق full-stack مع Next.js 15 و Firebase. يغطي هذا الدليل المصادقة، Firestore، التحديثات الفورية، Server Actions والنشر على Vercel.