الكتابات/tutorial/2026/06
Tutorial2 يونيو 2026·28 دقيقة

بناء تطبيق فوري باستخدام InstantDB و Next.js 15

تعلّم كيفية بناء تطبيق تعاوني فوري باستخدام InstantDB و Next.js 15. يغطي هذا الدرس تصميم المخطط، واستعلامات InstaQL، ومعاملات InstaML، والتحديثات التفاؤلية، والمصادقة بالرمز السحري، والحضور المباشر، والصلاحيات — البديل العصري لـ Firebase لمطوري الواجهات الأمامية.

InstantDB هي قاعدة بيانات فورية متعددة المستخدمين مخصّصة لتطبيقات الواجهة الأمامية — وغالباً ما توصف بأنها البديل العصري لـ Firebase. فبدلاً من ربط قاعدة بيانات وطبقة API وخادم WebSocket وذاكرة تخزين مؤقتة على جانب العميل بشكل منفصل، تمنحك InstantDB قاعدة بيانات واحدة على جانب العميل تتزامن فورياً عبر كل جهاز متصل، وتعمل دون اتصال، وتطبّق التحديثات التفاؤلية افتراضياً.

ما يميّز InstantDB في عام 2026 هو نموذج الرسم البياني العلائقي المقترن بالمزامنة الفورية. تُعرّف كيانات وروابط ذات أنواع، وتستعلم عنها بصياغة تصريحية صغيرة تُسمى InstaQL، وتعدّلها بمعاملات InstaML. كل تغيير ينعكس على الشاشة فوراً وينتشر إلى جميع العملاء الآخرين في الوقت الفعلي — دون اشتراكات يدوية ودون إبطال للذاكرة المؤقتة.

في هذا الدرس ستبني لوحة مهام تعاونية فورية — قائمة مشتركة يمكن لعدة مستخدمين فيها إضافة المهام وإكمالها وحذفها مع تزامن لحظي عبر المتصفحات. كما ستضيف المصادقة بالرمز السحري، وترى من غيرك متصل عبر ميزة الحضور، وتؤمّن بياناتك عبر الصلاحيات.

المتطلبات المسبقة

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

  • Node.js 20+ مثبّت
  • حساب InstantDB مجاني — سجّل عبر instantdb.com/dash
  • معرفة أساسية بـ React و TypeScript
  • إلمام بـ App Router في Next.js (التخطيطات، مكوّنات العميل، توجيه "use client")
  • محرّر أكواد (يُنصح بـ VS Code)

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

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

  • مخططاً ذا أنواع يربط tasks بـ $users
  • قراءات فورية عبر InstaQL
  • إنشاء وتحديث وحذف تفاؤلي عبر InstaML
  • مصادقة بالبريد عبر الرمز السحري
  • حضوراً مباشراً يُظهر من يشاهد اللوحة حالياً
  • قواعد صلاحيات من جهة الخادم بحيث لا يصل المستخدمون إلا إلى بياناتهم

لنبدأ.

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

أنشئ تطبيق Next.js 15 جديداً مع App Router و TypeScript:

npx create-next-app@latest instant-board --typescript --app --tailwind
cd instant-board

ثم ثبّت حزمة InstantDB لـ React وأداة سطر الأوامر:

npm install @instantdb/react
npm install -D instant-cli

تُوفّر حزمة @instantdb/react الـ hooks، ووسيط المعاملات، وعميل المصادقة الذي ستستخدمه طوال هذا الدرس.

الخطوة 2: تهيئة InstantDB

شغّل أداة سطر الأوامر لربط مشروعك المحلي بتطبيق InstantDB. سيؤدي ذلك إلى مصادقتك في المتصفح وإنشاء ملفّي المخطط والصلاحيات:

npx instant-cli@latest init

عند الطلب، أنشئ تطبيقاً جديداً (أو اختر تطبيقاً موجوداً). تكتب الأداة ملفّين في جذر مشروعك:

  • instant.schema.ts — نموذج البيانات ذو الأنواع
  • instant.perms.ts — قواعد التحكم بالوصول

كما تطبع App ID الخاص بك. أضِفه إلى ملف .env.local كي لا يُكتب مباشرة في الكود:

# .env.local
NEXT_PUBLIC_INSTANT_APP_ID=your-app-id-here

البادئة NEXT_PUBLIC_ مطلوبة كي يكون App ID متاحاً في المتصفح. ليس App ID سراً — فأمانك يأتي من قواعد الصلاحيات التي نضبطها في الخطوة 8.

الخطوة 3: تعريف المخطط

افتح instant.schema.ts وعرّف كياناً باسم tasks بالإضافة إلى رابط مع فضاء الأسماء المدمج $users. توفّر InstantDB الكيان $users تلقائياً عند استخدام المصادقة.

// instant.schema.ts
import { i } from "@instantdb/react";
 
const _schema = i.schema({
  entities: {
    $users: i.entity({
      email: i.string().unique().indexed(),
    }),
    tasks: i.entity({
      text: i.string(),
      done: i.boolean(),
      createdAt: i.date().indexed(),
    }),
  },
  links: {
    taskOwner: {
      forward: { on: "tasks", has: "one", label: "owner" },
      reverse: { on: "$users", has: "many", label: "tasks" },
    },
  },
});
 
// مساعِدات TypeScript لأمان الأنواع من الطرف إلى الطرف
type _AppSchema = typeof _schema;
interface AppSchema extends _AppSchema {}
const schema: AppSchema = _schema;
 
export type { AppSchema };
export default schema;

بعض النقاط الجديرة بالملاحظة:

  • تُعرّف i.string() و i.boolean() و i.date() سماتٍ ذات أنواع.
  • تجعل .indexed() الحقل سريع التصفية والترتيب — افهرس دائماً الحقول التي تستعلم عنها.
  • يُعرّف الرابط taskOwner علاقة واحد إلى متعدد: لكل مهمة مالك واحد، ولكل مستخدم مهام متعددة.

ادفع المخطط إلى InstantDB كي تعرفه السحابة:

npx instant-cli@latest push schema

الخطوة 4: تهيئة العميل

أنشئ نسخة db واحدة مشتركة. وبما أننا نمرّر المخطط، فإن كل استعلام ومعاملة سيخضعان لفحص الأنواع بالكامل.

// lib/db.ts
import { init } from "@instantdb/react";
import schema from "../instant.schema";
 
export const db = init({
  appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!,
  schema,
});

لتطبيقات Next.js التي تحتاج إلى تصيير المحتوى المصادَق عليه من جهة الخادم، توفّر InstantDB أيضاً init من @instantdb/react/nextjs، الذي يزامن جلسة المصادقة عبر مسار API من الطرف الأول. أما هذه اللوحة المُصيَّرة على العميل فيكفيها الاستيراد القياسي من @instantdb/react.

الخطوة 5: إضافة المصادقة بالرمز السحري

أبسط طرق المصادقة في InstantDB هي الرموز السحرية: يُدخل المستخدم بريداً إلكترونياً، فيتلقّى رمزاً من ستة أرقام، ثم يلصقه. لا كلمات مرور ولا إعداد OAuth.

أنشئ مكوّن تسجيل الدخول:

// components/Login.tsx
"use client";
 
import { useState } from "react";
import { db } from "@/lib/db";
 
export function Login() {
  const [sentEmail, setSentEmail] = useState("");
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
 
  const sendCode = (e: React.FormEvent) => {
    e.preventDefault();
    setSentEmail(email);
    db.auth.sendMagicCode({ email }).catch((err) => {
      alert("Error: " + err.body?.message);
      setSentEmail("");
    });
  };
 
  const verifyCode = (e: React.FormEvent) => {
    e.preventDefault();
    db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => {
      alert("Error: " + err.body?.message);
      setCode("");
    });
  };
 
  if (!sentEmail) {
    return (
      <form onSubmit={sendCode} className="flex flex-col gap-3 max-w-sm">
        <h2 className="text-xl font-bold">Sign in</h2>
        <input
          type="email"
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="border px-3 py-2 rounded"
          required
        />
        <button className="bg-blue-600 text-white px-3 py-2 rounded">
          Send code
        </button>
      </form>
    );
  }
 
  return (
    <form onSubmit={verifyCode} className="flex flex-col gap-3 max-w-sm">
      <h2 className="text-xl font-bold">Enter your code</h2>
      <p>We emailed a code to {sentEmail}.</p>
      <input
        type="text"
        placeholder="123456"
        value={code}
        onChange={(e) => setCode(e.target.value)}
        className="border px-3 py-2 rounded"
        required
      />
      <button className="bg-blue-600 text-white px-3 py-2 rounded">
        Verify
      </button>
    </form>
  );
}

النداءان اللذان يقومان بكل العمل هما db.auth.sendMagicCode({ email }) و db.auth.signInWithMagicCode({ email, code }). وعند النجاح، تخزّن InstantDB الجلسة وتُحدّث كل مكوّن يقرأ حالة المصادقة.

الخطوة 6: حماية التطبيق عبر useAuth

استخدم الـ hook المسمى db.useAuth() لتقرر ما إذا كنت ستعرض شاشة تسجيل الدخول أم اللوحة. فهو يُرجع حالة التحميل، والمستخدم الحالي، وأي خطأ.

// app/page.tsx
"use client";
 
import { db } from "@/lib/db";
import { Login } from "@/components/Login";
import { Board } from "@/components/Board";
 
export default function Home() {
  const { isLoading, user, error } = db.useAuth();
 
  if (isLoading) return <div className="p-8">Loading...</div>;
  if (error) return <div className="p-8">Auth error: {error.message}</div>;
 
  return (
    <main className="p-8 max-w-2xl mx-auto">
      {user ? <Board user={user} /> : <Login />}
    </main>
  );
}

الخطوة 7: القراءة والكتابة في الوقت الفعلي

والآن إلى قلب التطبيق. أنشئ مكوّن Board الذي يقرأ المهام عبر InstaQL ويعدّلها عبر InstaML.

// components/Board.tsx
"use client";
 
import { useState } from "react";
import { id, type User } from "@instantdb/react";
import { db } from "@/lib/db";
 
export function Board({ user }: { user: User }) {
  const [text, setText] = useState("");
 
  // InstaQL: اقرأ مهام المستخدم الحالي مرتبة حسب وقت الإنشاء
  const { isLoading, error, data } = db.useQuery({
    tasks: {
      $: {
        where: { "owner.id": user.id },
        order: { createdAt: "desc" },
      },
    },
  });
 
  const addTask = (e: React.FormEvent) => {
    e.preventDefault();
    if (!text.trim()) return;
    const taskId = id();
    // InstaML: أنشئ المهمة واربطها بالمستخدم في معاملة واحدة
    db.transact(
      db.tx.tasks[taskId]
        .update({ text, done: false, createdAt: Date.now() })
        .link({ owner: user.id }),
    );
    setText("");
  };
 
  const toggle = (taskId: string, done: boolean) => {
    db.transact(db.tx.tasks[taskId].update({ done: !done }));
  };
 
  const remove = (taskId: string) => {
    db.transact(db.tx.tasks[taskId].delete());
  };
 
  if (isLoading) return <div>Loading tasks...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">Task Board</h1>
        <button onClick={() => db.auth.signOut()} className="text-sm underline">
          Sign out
        </button>
      </div>
 
      <form onSubmit={addTask} className="flex gap-2">
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="What needs doing?"
          className="border px-3 py-2 rounded flex-1"
        />
        <button className="bg-blue-600 text-white px-4 rounded">Add</button>
      </form>
 
      <ul className="flex flex-col gap-2">
        {data.tasks.map((task) => (
          <li
            key={task.id}
            className="flex items-center gap-3 border rounded px-3 py-2"
          >
            <input
              type="checkbox"
              checked={task.done}
              onChange={() => toggle(task.id, task.done)}
            />
            <span className={task.done ? "line-through opacity-60" : ""}>
              {task.text}
            </span>
            <button
              onClick={() => remove(task.id)}
              className="ml-auto text-red-600 text-sm"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

ثلاثة مفاهيم تُشغّل هذا المكوّن:

  1. InstaQL — إن db.useQuery({ tasks: { ... } }) اشتراك حيّ. يحمل المعامل $ خيارات الاستعلام مثل where و order. وعندما يغيّر أي عميل مهمة مطابقة، يُعاد تصيير هذا المكوّن تلقائياً.
  2. InstaML — تصف db.transact(db.tx.tasks[taskId].update(...)) عملية تعديل. يتبع وسيط db.tx الشكل db.tx.NAMESPACE[ID].ACTION(DATA)، حيث تشمل الإجراءات create و update و merge و delete و link و unlink.
  3. id() — يولّد معرّف UUID جديداً للكيانات الجديدة كي تبني الواجهة التفاؤلية قبل أن يستجيب الخادم.

ولأن InstantDB تطبّق التحديثات التفاؤلية محلياً، فإن المهمة الجديدة تظهر في اللحظة التي ترسل فيها النموذج — حتى قبل اكتمال الرحلة إلى الخادم وعودتها.

الخطوة 8: التأمين عبر الصلاحيات

افتراضياً، يكون تطبيق InstantDB مفتوحاً كي تتمكن من بناء النماذج الأولية بسرعة. لكن قبل الإطلاق يجب تقييد الوصول. افتح instant.perms.ts واشترط ألا يقرأ المستخدمون ويكتبوا إلا مهامهم الخاصة.

// instant.perms.ts
import type { InstantRules } from "@instantdb/react";
 
const rules = {
  tasks: {
    allow: {
      view: "auth.id != null && auth.id == data.ref('owner.id')",
      create: "auth.id != null && auth.id == newData.ref('owner.id')",
      update: "auth.id != null && auth.id == data.ref('owner.id')",
      delete: "auth.id != null && auth.id == data.ref('owner.id')",
    },
  },
} satisfies InstantRules;
 
export default rules;

تستخدم هذه القواعد auth.id (معرّف المستخدم المصادَق عليه) و data.ref('owner.id') (مالك المهمة المرتبط) لضمان ألا يستطيع أحد قراءة أو تعديل مهام لا يملكها. ادفع القواعد إلى السحابة:

npx instant-cli@latest push perms

لا تُطلق تطبيقاً أبداً بصلاحياته الافتراضية المفتوحة. فـ App ID علني، ويمكن لأي شخص قراءة بياناتك أو الكتابة عليها إلى أن تُطبَّق القواعد. اختبر قواعدك بتسجيل الدخول بمستخدمَين مختلفين والتأكد من أن كلّاً منهما لا يرى إلا مهامه.

الخطوة 9: إضافة الحضور المباشر

يُظهر الحضور من يشاهد اللوحة حالياً في الوقت الفعلي، دون تخزين أي شيء في قاعدة البيانات. أنشئ غرفة وانشر حالة كل مستخدم.

// components/PresenceBar.tsx
"use client";
 
import { db } from "@/lib/db";
import type { User } from "@instantdb/react";
 
const room = db.room("board", "main");
 
export function PresenceBar({ user }: { user: User }) {
  const { peers } = db.rooms.usePresence(room);
 
  db.rooms.useSyncPresence(room, { email: user.email });
 
  const others = Object.values(peers);
 
  return (
    <div className="text-sm text-gray-600">
      {others.length === 0
        ? "You're the only one here"
        : `${others.length} other ${others.length === 1 ? "person" : "people"} online: ` +
          others.map((p) => p.email).join(", ")}
    </div>
  );
}

هنا يُعرّف db.room("board", "main") قناة مشتركة عابرة. ينشر useSyncPresence بيانات المستخدم الحالي، ويُرجع usePresence الـ peers الموجودين حالياً في الغرفة. أضِف <PresenceBar user={user} /> إلى Board ليصبح لديك فوراً مؤشّر مباشر لـ "من المتصل". وتُشغّل البِنية نفسها للغرفة مؤشرات الكتابة، والمؤشرات الحية، والتفاعلات.

اختبار التنفيذ

  1. شغّل npm run dev وافتح http://localhost:3000.
  2. سجّل الدخول ببريدك والرمز السحري.
  3. أضِف بضع مهام — لاحظ ظهورها فوراً بفضل التحديثات التفاؤلية.
  4. افتح التطبيق في نافذة متصفح ثانية (أو تبويب خفي) وسجّل الدخول ببريد مختلف. تأكد من أن كل حساب لا يرى إلا مهامه (قواعد الصلاحيات تعمل).
  5. افتح الحساب نفسه في تبويبين وراقب تزامن المهام في الوقت الفعلي بينهما.
  6. راقب تحديث شريط الحضور أثناء فتح التبويبات وإغلاقها.

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

  • "Permission denied" على كل استعلام. على الأرجح تشير قواعدك إلى رابط غير موجود، أو نسيت دفعها. شغّل npx instant-cli@latest push perms وتأكد من وجود الرابط taskOwner في مخططك.
  • لا تظهر المهام بعد التحديث. تأكد من دفعك للمخطط (push schema) ومن أن createdAt مفهرس — فالترتيب حسب حقل غير مفهرس يفشل.
  • أخطاء أنواع على db.useQuery. تأكد من أن lib/db.ts يمرّر وسيط schema إلى init. فبدونه تكون الاستعلامات بلا أنواع.
  • لا يصل الرمز السحري أبداً. تحقق من البريد المزعج، وتأكد من تطابق App ID في .env.local مع لوحة التحكم. أعد تشغيل خادم التطوير بعد تعديل ملفات البيئة.

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

  • أضِف مؤشرات حية باستخدام بِنية db.room نفسها مع useTopicEffect للأحداث العابرة.
  • شارِك اللوحات بين المستخدمين بإضافة كيان boards مع رابط متعدد إلى متعدد مع $users.
  • انتقل إلى جهة الخادم عبر حزمة @instantdb/admin لتعبئة البيانات أو تنفيذ معاملات موثوقة من مسارات API.
  • استكشف الدروس ذات الصلة على هذا الموقع: تطبيقات Convex الفورية، ومزامنة ElectricSQL، والتطبيقات المحلية أولاً باستخدام Yjs.

الخاتمة

في أقل من 30 دقيقة بنيت لوحة مهام فورية، متعددة المستخدمين، قادرة على العمل دون اتصال، باستخدام InstantDB و Next.js 15 — دون مسارات API، ودون أسلاك WebSocket، ودون إدارة للذاكرة المؤقتة. لقد عرّفت مخططاً علائقياً ذا أنواع، وقرأت البيانات عبر InstaQL، وعدّلتها عبر InstaML، وصادقت المستخدمين بالرموز السحرية، وأمّنت كل شيء بقواعد الصلاحيات، وأضفت الحضور المباشر.

وعد InstantDB هو أن الفورية والعمل دون اتصال ليسا ميزتين تضيفهما لاحقاً — بل هما الإعداد الافتراضي. وهذا التحوّل يتيح لمطوري الواجهات الأمامية إطلاق برمجيات تعاونية بالجهد نفسه الذي كان يلزم سابقاً لبناء تطبيق CRUD ثابت.