> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Headless CMS: متى تحتاجه، وهل تختار Strapi أم Directus؟
- URL: https://arabroot.io/articles/cms-بلا-واجهة-headless-strapi-ام-directus/
- Published: 2026-10-08T08:00:00.000Z
- Updated: 2026-10-09T13:02:05.000Z
- Description: متى تحتاج نظام محتوى يقدم المحتوى عبر API بدل الصفحات، وكيف تحمي المسودات من التسريب عند الاختيار بين Strapi وDirectus.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك موقعاً لشركتك يعمل على WordPress، ثم قرر الفريق بناء تطبيق للجوال يعرض الأخبار نفسها، ثم طلبت الإدارة شاشة في الاستقبال تعرض آخر الإعلانات، فستجد أن المحتوى محبوس داخل قوالب الموقع وأن كل واجهة جديدة تحتاج إلى نسخ المحتوى أو إلى حيل لاستخراجه من صفحات HTML. والحل الذي انتشر لهذه المشكلة هو **نظام إدارة المحتوى بلا واجهة Headless CMS**، أي نظام يحتفظ بلوحة التحرير وقاعدة البيانات ويتخلى عن عرض الصفحات، ثم يقدم المحتوى عبر API لأي واجهة تطلبه.

وهذا المقال هو أول مقالات مرحلة [منصات للمطورين](https://arabroot.io/start/#stage-devplatform) في خارطة الطريق، وسوف نناقش فيه ما يلي:

- ما الفرق بين ال CMS التقليدي وال Headless، ومتى تحتاج إليه ومتى لا تحتاج.
- نموذج المحتوى Content Model وال API الذي يتولد منه بصيغتي REST وGraphQL.
- نوعا الصلاحيات: من يكتب في لوحة التحكم، ومن يقرأ عبر ال API، ولماذا تتسرب المسودات.
- أشهر أداتين تستضيفهما بنفسك: Strapi وDirectus، وترخيص كل منهما كما هو اليوم بعد تغيير Directus لترخيصه في الإصدار 12.
- تجربة عملية على الأداتين بالنموذج نفسه، وفيها تسريب حقيقي للمسودات في Strapi وإصلاحه، وحدود الطبقة المجانية في Directus كما ظهرت لنا.

## ما هو نظام إدارة المحتوى بلا واجهة Headless CMS؟

نظام إدارة المحتوى Content Management System، واختصاراً CMS، هو برنامج يسمح لشخص غير مبرمج بأن يكتب المحتوى وينظمه وينشره. وفي الشكل التقليدي Coupled CMS مثل WordPress وGhost وDrupal يكون كل شيء في تطبيق واحد: لوحة التحكم التي يكتب فيها المحرر، وقاعدة البيانات، والقالب Theme الذي يحول المحتوى إلى صفحات HTML يراها الزائر. وكلمة Head في الاسم تعني هذا الجزء الأخير، أي طبقة العرض، فال Headless CMS هو CMS حذفت منه طبقة العرض، وبقي له شيئان: لوحة تحرير لفريق المحتوى، و API يعيد المحتوى بصيغة JSON لمن يطلبه. والصورة التالية تبين الفرق بين الشكلين:

![في الأعلى CMS تقليدي فيه لوحة التحكم وقاعدة البيانات والقالب داخل تطبيق واحد يرسل صفحة HTML إلى الموقع فقط، وتطبيق الجوال عليه علامة خطأ، وفي الأسفل Headless CMS فيه لوحة التحكم وقاعدة البيانات فقط، ثم Content API بصيغة REST وGraphQL يرسل JSON إلى موقع وتطبيق جوال وشاشة وخدمة أخرى](https://arabroot.io/content/images/2026/10/headless-cms-01-coupled-vs-headless.webp)

المحتوى نفسه يخرج من مكان واحد، وكل واجهة تعرضه بطريقتها

وفي الصورة أعلاه لاحظ أن الموقع في الشكل الثاني أصبح تطبيقاً منفصلاً يبنيه فريق الواجهة بإطار مثل Next.js أو Astro أو Nuxt، ويطلب المحتوى من ال API عند البناء أو عند كل طلب، وأن تطبيق الجوال والشاشة يقرآن من ال API نفسه دون أي نسخ. ويوجد بين الشكلين شكل ثالث يسمى المنفصل Decoupled، وهو CMS تقليدي يحتفظ بقوالبه ويقدم API إلى جانبها، مثل [REST API في WordPress](https://developer.wordpress.org/rest-api/?ref=arabroot.io) و[Content API في Ghost](https://ghost.org/docs/content-api/?ref=arabroot.io)، وهو حل جيد إذا كان الموقع هو الواجهة الأساسية والتطبيق إضافة صغيرة.

### متى تحتاج إليه، ومتى لا تحتاج؟

وقد يتساءل البعض: إذا كان ال Headless يخدم كل الواجهات، فلماذا لا نستخدمه دائماً؟ والإجابة أنه ينقل جزءاً كبيراً من العمل إليك، فالقالب الذي كان جاهزاً في WordPress أصبح تطبيقاً تكتبه وتختبره وتستضيفه، والمعاينة Preview التي كانت زراً واحداً أصبحت ميزة تبنيها بين تطبيقين، والروابط وخرائط الموقع Sitemap وبيانات محركات البحث SEO أصبحت مسؤولية فريق الواجهة. لذلك قاعدة الاختيار كما يلي:

- **اختر Headless** عندما يكون للمحتوى أكثر من واجهة (موقع وتطبيق جوال وشاشات أو خدمات أخرى)، أو عندما يكون عندك فريق واجهة يعمل بإطار حديث ويريد التحكم الكامل في العرض، أو عندما يكون المحتوى جزءاً من تطبيق وليس موقعاً، مثل كتالوج منتجات أو أسئلة شائعة داخل نظام.
- **لا تختره** لمدونة أو موقع تعريفي يديره شخص واحد، فهنا يكفيك [WordPress](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-wordpress-%D9%85%D8%B9-docker-compose/) أو Ghost، والسبب أنك سوف تدير تطبيقين بدل تطبيق واحد دون فائدة تذكر.

## نموذج المحتوى Content Model

أول ما تفعله في أي Headless CMS هو تعريف نموذج المحتوى Content Model، أي أنواع المحتوى Content Types التي يكتبها فريقك، والحقول Fields التي في كل نوع، والعلاقات Relations بين الأنواع. فالمقال مثلاً له عنوان Title نصي إجباري، ومعرف في الرابط Slug لا يتكرر، ونص Body منسق، وصورة غلاف Cover، وعلاقة مع التصنيف Category، وحالة مسودة أو منشور Draft and Published، وربما لغة Locale إذا كان الموقع بأكثر من لغة. والأداة تحول هذا التعريف إلى جداول في قاعدة البيانات، وتولد منه API كاملاً دون أن تكتب سطراً، كما تبين الصورة التالية:

![بطاقة نوع محتوى Article بحقول title وslug وbody وcover وcategory وstatus وlocale، وبطاقة Category مرتبطة بعلاقة many to one، ثم ترس CMS ينشئ جداول قاعدة البيانات، ويولد مسارات REST مثل GET وPOST وPUT وDELETE على /api/articles مع filters وpopulate، ومخطط GraphQL مع استعلام يطلب title وslug وcategory name](https://arabroot.io/content/images/2026/10/headless-cms-02-content-model.webp)

تعريف واحد للنموذج يعطيك الجداول ومسارات REST ومخطط GraphQL معاً

وفي الصورة أعلاه لاحظ التالي:

- **أنواع المجموعات Collection Types** مثل المقالات والتصنيفات لها عدد كبير من العناصر، وفي المقابل يوجد **النوع المفرد Single Type** لمحتوى له نسخة واحدة فقط، مثل إعدادات الصفحة الرئيسية أو صفحة من نحن.
- **المكونات Components** مجموعة حقول تتكرر في أكثر من نوع، مثل بيانات SEO (العنوان والوصف والصورة)، فتعرفها مرة واحدة وتضيفها حيث تحتاج.
- **المسودة والنشر Draft and Publish** يجعل لكل عنصر نسختين، واحدة يعمل عليها المحرر وأخرى يراها الزوار، وهذا مفيد جداً وخطير في الوقت نفسه كما سوف نرى في التجربة.
- التصميم الجيد للنموذج يشبه تصميم قاعدة البيانات، فلا تكرر البيانات بين الأنواع ولا تضع في حقل نصي ما يجب أن يكون علاقة، وقد شرحنا المبادئ في [تصميم قواعد البيانات](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/).

## ال API الذي تحصل عليه: REST وGraphQL

من النموذج تولد الأداة مسارات REST لكل نوع: قائمة العناصر وعنصر واحد والإنشاء والتعديل والحذف، مع معاملات للتصفية Filtering والترتيب Sorting والتقسيم إلى صفحات Pagination، ومعامل لجلب العلاقات مع العنصر (يسمى `populate` في Strapi، وتكتب العلاقة داخل `fields` في Directus). وتولد أيضاً مخطط GraphQL يسمح للواجهة بأن تطلب الحقول التي تحتاجها فقط في طلب واحد. وقد شرحنا الفرق بين الأسلوبين ومتى تختار كلاً منهما في [تصميم واجهات API](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/#graphql)، والمهم هنا ثلاث نقاط خاصة بال CMS:

- **الواجهة لا تحتاج إلى كل الحقول:** اطلب الحقول التي تعرضها فقط، والسبب أن الرد الكامل مع العلاقات قد يكون أكبر من الصفحة نفسها، وقد يحمل حقولاً لم تقصد نشرها.
- **GraphQL يحتاج إلى حدود:** استعلام متداخل بعمق كبير أو يطلب آلاف العناصر يستطيع أن يشغل السيرفر، وهذا ما نبهنا إليه Strapi نفسه في السجل عند التشغيل كما سوف ترى في التجربة.
- **الموقع الثابت Static Site يحتاج إلى إعادة بناء:** إذا كانت الواجهة تبنى مرة واحدة ثم ترفع إلى [شبكة CDN](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، فالمحتوى الجديد لا يظهر حتى يعاد البناء، والحل المعتاد هو Webhook يرسله ال CMS عند النشر إلى خط CI ليبني الموقع من جديد.

## الصلاحيات: لوحة التحكم شيء، وال API شيء آخر

أكثر ما يخلط فيه المبتدئ في ال Headless CMS هو الصلاحيات، والسبب أن فيه نظامين منفصلين تماماً كما تبين الصورة التالية:

![على اليسار مستخدمو لوحة التحكم: Admin يغير النموذج والمستخدمين، وEditor يعدل وينشر كل المحتوى، وAuthor يعدل مسوداته فقط، ويدخلون إلى /admin بتسجيل دخول وتحقق ثنائي أو SSO، وعلى اليمين مستهلكو ال API: الجمهور دون توكن يقرأ المنشور فقط، وتوكن سيرفر البناء للقراءة فقط مع المسودات للمعاينة، ومستخدمو التطبيق ب JWT لتعليقاتهم وملفهم، وفي الأسفل تحذير أن المسودة الظاهرة للجمهور تسريب قبل يوم الإطلاق](https://arabroot.io/content/images/2026/10/headless-cms-03-two-kinds-of-access.webp)

صلاحيات المحررين في لوحة التحكم منفصلة عن صلاحيات من يطلب المحتوى عبر ال API

- **صلاحيات لوحة التحكم Admin RBAC:** تحدد من يغير نموذج المحتوى، ومن ينشر، ومن يكتب مسودات فقط، وهي نظام صلاحيات حسب الدور Role-Based Access Control الذي شرحناه في [الصلاحيات المركزية](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/).
- **صلاحيات ال API:** تحدد ما يراه من يطلب المحتوى، وفيها ثلاث فئات: الجمهور Public دون أي توكن، وهو كل زائر لموقعك وكل من يعرف رابط ال API، والتوكن الثابت API Token لسيرفر البناء أو لخدمة أخرى، ومستخدمو تطبيقك إذا كان ال CMS يدير حساباتهم عبر JWT كما شرحنا في [الجلسات و JWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

⛔

لا تضع توكن ال CMS في كود الواجهة الذي يصل إلى المتصفح أو في تطبيق الجوال، والسبب أن أي شخص يفتح أدوات المطور يقرأه، فإذا كان التوكن بصلاحية كاملة Full Access أصبح يستطيع تعديل محتواك وحذفه. التوكن مكانه سيرفر البناء أو سيرفر الواجهة، والمتصفح يقرأ ما هو منشور فقط عبر صلاحيات الجمهور.

## Strapi: النموذج ملفات في مشروعك

[Strapi](https://strapi.io/?ref=arabroot.io) هو أشهر Headless CMS مفتوح المصدر مكتوب ب Node.js، وتنشئ به مشروعاً كما تنشئ أي مشروع Node، ثم تعرف النموذج إما من الواجهة الرسومية Content-Type Builder أثناء التطوير أو بكتابة ملفات `schema.json` مباشرة، وفي الحالتين يصبح النموذج ملفات داخل المشروع تحفظ في Git وتراجع في طلبات الدمج وتنشر مع الكود. ولهذا السبب يمنع Strapi تعديل النموذج من لوحة التحكم في الإنتاج، فالنموذج يتغير في بيئة التطوير ثم ينتقل بالنشر، وعند التشغيل ينشئ Strapi الجداول والأعمدة التي تنقص في قاعدة البيانات. ويدعم SQLite وPostgreSQL وMySQL وMariaDB.

أما الترخيص فالملف [LICENSE](https://github.com/strapi/strapi/blob/develop/LICENSE?ref=arabroot.io) في المستودع ينص على أن كل الكود خارج المجلدات المسماة `ee/` بترخيص MIT، وأن ما في مجلدات `ee/` هو النسخة التجارية Enterprise Edition ويخضع لاتفاقية Strapi التجارية. وعملياً تحصل في النسخة المجانية (تسمى في لوحة التحكم Community) على النموذج والمسودات والنشر وتعدد اللغات i18n ومكتبة الوسائط وال API Tokens والصلاحيات RBAC مع أدوار مخصصة، بينما تحتاج بحسب [التوثيق الرسمي](https://docs.strapi.io/cms/intro?ref=arabroot.io) إلى خطة Growth لسجل تغييرات المحتوى Content History والإصدارات Releases، وإلى خطة Enterprise لسجل التدقيق Audit Logs ومسارات المراجعة Review Workflows، و[الدخول الموحد SSO](https://docs.strapi.io/cms/features/sso?ref=arabroot.io) متاح في Enterprise أو كإضافة مدفوعة على Growth.

ويجمع Strapi بيانات استخدام مجهولة Telemetry كما في [صفحة بيانات الاستخدام](https://docs.strapi.io/cms/usage-information?ref=arabroot.io)، ومنها معرف للمشروع ومعرف للجهاز ونظام التشغيل، وتوقفها بالمتغير `STRAPI_TELEMETRY_DISABLED=true` أو بالأمر `npm run strapi telemetry:disable`.

**نقاط القوة:**

- ترخيص MIT للنسخة المجانية، وهي تكفي أغلب المواقع والتطبيقات دون حدود على عدد المستخدمين أو أنواع المحتوى.
- النموذج كود في Git، فتراجعه وتنقله بين البيئات مع باقي المشروع دون أدوات إضافية.
- مشروع Node عادي تضيف إليه منطقك الخاص: Controllers وServices وMiddlewares وإضافات Plugins.

**نقاط الضعف:**

- لا توجد Docker Image رسمية جاهزة، فأنت تبني مشروعك ثم تصنع له Docker Image بنفسك، وكل تحديث للإصدار تحديث لمشروع Node بكل اعتمادياته.
- يفترض أنه يملك قاعدة البيانات، فلا يناسب وضعه فوق قاعدة بيانات موجودة لنظام آخر.
- سجل التدقيق ومسارات المراجعة والدخول الموحد في الخطط المدفوعة، وهي مهمة للجهات التي تحتاج إلى معرفة من نشر ماذا.

## Directus: لوحة تحكم و API فوق قاعدة بياناتك

[Directus](https://directus.com/?ref=arabroot.io) يعمل بطريقة مختلفة، فهو لا يفترض أنه يملك قاعدة البيانات، وإنما يقرأ أي قاعدة بيانات SQL كما هي، الجديدة أو الموجودة لنظام آخر، فيحول جداولها إلى مجموعات Collections لها لوحة تحرير و API بصيغتي REST وGraphQL مباشرة. ويحفظ ما يضيفه على الجداول (طريقة عرض الحقل والعلاقات والصلاحيات) في جداول خاصة به تبدأ ب `directus_`، وتنقل النموذج بين البيئات بملف Snapshot تأخذه من بيئة وتطبقه على أخرى. ويأتي ك Docker Image رسمية جاهزة. والصورة التالية تقارن بين طريقة الأداتين:

![في الأعلى Strapi: المطور يستخدم Content-Type Builder في بيئة التطوير فينتج ملف schema.json داخل المشروع، ثم Git للمراجعة، ثم النشر إلى Strapi في الإنتاج حيث المحرر معطل، ثم Strapi ينشئ الجداول عند التشغيل، وفي الأسفل Directus: قاعدة بيانات SQL جديدة أو موجودة يقرؤها Directus ويحفظ بياناته الوصفية في جداول directus_، ثم أمر schema snapshot ينتج snapshot.yaml يحفظ في Git، ثم schema diff وapply على Directus في الإنتاج فتتغير الجداول لتطابقه](https://arabroot.io/content/images/2026/10/headless-cms-04-code-vs-database.webp)

في Strapi الأصل هو ملفات الكود، وفي Directus الأصل هو قاعدة البيانات نفسها

### ترخيص Directus: ثلاث مراحل في ست سنوات

ترخيص Directus هو أهم ما تقرأه قبل اختياره، لأنه تغير مرتين، وما تقرؤه في المقالات القديمة لم يعد صحيحاً:

1. **حتى الإصدار 9:** كان Directus بترخيص GPLv3 مفتوح المصدر.
2. **من الإصدار 10 (أبريل 2023):** انتقل إلى [Business Source License 1.1](https://github.com/directus/directus/pull/18330?ref=arabroot.io)، مع منحة استخدام إضافية Additional Use Grant تسمح بالاستخدام في الإنتاج مجاناً ما دام مجموع أموال الجهة Total Finances لا يتجاوز 5 ملايين دولار، ومن يتجاوزها يحتاج إلى ترخيص تجاري، ويتحول كل إصدار إلى GPLv3 بعد ثلاث سنوات من صدوره.
3. **من الإصدار 12 (يونيو 2026):** انتقل كما في [ملاحظات الإصدار](https://github.com/directus/directus/releases/tag/v12.0.0?ref=arabroot.io) إلى ترخيص جديد اسمه [Monospace Sustainable Core License](https://github.com/directus/directus/blob/main/license?ref=arabroot.io) واختصاره MSCL-1.0-GPL، وأضاف لأول مرة مفتاح ترخيص License Key يفحصه البرنامج نفسه.

والترخيص الحالي يسمح لك باستخدام البرنامج وتعديله لأي غرض ما عدا الاستخدام المنافس Competing Use، أي تقديم Directus نفسه لجهات أخرى كخدمة تنافس عروض الشركة المدفوعة، ويمنع تعطيل فحص مفتاح الترخيص أو الالتفاف عليه، ويمنحك ترخيص GPL-3.0 لكل إصدار بعد مرور أربع سنوات على صدوره. ولأن الترخيص يقيد مجال الاستخدام فهو ليس ترخيصاً مفتوح المصدر بتعريف OSI، وإنما من فئة الكود المتاح Source Available التي شرحناها في [مقال التراخيص](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/#source-available).

وأما ما تحصل عليه فعلاً، فبحسب [صفحة الترخيص](https://directus.com/docs/licensing/overview?ref=arabroot.io) و[صفحة الأسعار](https://directus.com/pricing?ref=arabroot.io) كما قرأناهما في أكتوبر 2026:

- **الطبقة الأساسية Core:** مجانية لأي جهة مهما كان حجمها ودون مفتاح، وفيها 3 مستخدمين للوحة التحكم Seats، و25 مجموعة Collections، و5 سير عمل Flows، وسجل نشاط ومراجعات لمدة 30 يوماً. والمستخدمون الذين يصلون عبر ال API فقط (مستخدمو تطبيقك وحسابات الخدمات) لا يحسبون من المقاعد.
- **ما لا تحصل عليه في Core:** الدخول الموحد SSO، وقواعد الصلاحيات المخصصة Custom Permission Rules (مثل «الجمهور يرى المنشور فقط»)، وإيقاف إرسال بيانات الاستخدام Telemetry، وسوف ترى أثر الثانية في التجربة.
- **منحة الابتكار المفتوح Open Innovation Grant:** مفتاح مجاني بكل الميزات للجهات التي إيراداتها السنوية أقل من 5 ملايين دولار وعدد موظفيها أقل من 50 موظفاً، وتقدر الأهلية بحسب الجهات التي يدخل ممثلوها إلى لوحة التحكم.
- **الخطط المدفوعة:** Team بسعر 499 دولاراً شهرياً مع التزام سنوي، وEnterprise بسعر يحدد بالتفاوض.
- **الاتصال بالشركة:** المفتاح يتحقق دورياً من خدمة الترخيص على `licensing.directus.com`، والوضع دون اتصال Offline متاح في Enterprise فقط. وبحسب [صفحة بيانات الاستخدام](https://directus.com/docs/licensing/telemetry?ref=arabroot.io) يرسل البرنامج كل 6 ساعات تقريراً فيه رابط مشروعك العام وأعداد المجموعات والعناصر والمستخدمين، وبعض التراخيص تتجاهل المتغير `TELEMETRY=false`، وفي صفحة الأسعار إيقافه من ميزات Enterprise فقط.

⚠️

إذا كانت جهتك حكومية أو تحتاج إلى بقاء البيانات داخل الدولة، فاقرأ نقطة الاتصال بالشركة بعناية قبل اختيار Directus، والسبب أن السيرفر يرسل رابط مشروعك وإحصاءات عنه إلى خارج شبكتك كل 6 ساعات، ولا تستطيع إيقاف ذلك في الطبقة المجانية، وقد شرحنا ما يعنيه ذلك في [السيادة على البيانات](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9-%D9%85%D8%AA%D9%89-%D8%AA%D8%B5%D8%A8%D8%AD-%D8%AE%D9%8A%D8%A7%D8%B1%D8%A7-%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%D9%8A%D8%AC%D9%8A%D8%A7-%D9%84%D8%A3%D8%B9%D9%85%D8%A7%D9%84%D9%83/#sovereignty).

**نقاط القوة:**

- يعمل فوق قاعدة بيانات موجودة دون أن يغير شكلها، وهذا لا تجده في Strapi.
- Docker Image رسمية جاهزة، والترقية بتغيير الوسم Tag.
- لوحة تحكم غنية: لوحات Dashboards وسير عمل Flows ومكتبة ملفات، مع GraphQL مضمن دون إضافة.

**نقاط الضعف:**

- الترخيص ليس مفتوح المصدر، وتغير مرتين، ويقيد الطبقة المجانية بثلاثة مقاعد و25 مجموعة.
- قواعد الصلاحيات حسب قيمة الحقل غير متاحة في Core، فلا تستطيع أن تقول «الجمهور يرى المنشور فقط» دون مفتاح.
- اتصال إلزامي بخدمات الشركة للترخيص وبيانات الاستخدام، إلا في Enterprise.

## المقارنة في جدول واحد

| البند                   | Strapi 5                                           | Directus 12                                                               |
| ----------------------- | -------------------------------------------------- | ------------------------------------------------------------------------- |
| الترخيص                 | MIT، وما في مجلدات ee/ تجاري                       | MSCL-1.0-GPL (Source Available)، ويصبح GPL-3.0 بعد 4 سنوات                |
| الطبقة المجانية         | دون حدود على المستخدمين أو الأنواع                 | 3 مقاعد و25 مجموعة و5 Flows، أو منحة لمن هم دون 5 ملايين دولار و50 موظفاً |
| مصدر النموذج            | ملفات schema.json في المشروع                       | قاعدة البيانات، وينقل بملف Snapshot                                       |
| قاعدة بيانات موجودة     | غير مناسب                                          | مصمم لذلك                                                                 |
| GraphQL                 | إضافة رسمية @strapi/plugin-graphql                 | مضمن                                                                      |
| الجمهور يرى المنشور فقط | افتراضياً، لكن status=draft يتجاوزه (انظر التجربة) | يحتاج إلى قاعدة مخصصة، وهي غير متاحة في Core                              |
| SSO                     | Enterprise أو إضافة على Growth                     | Team وما فوق، أو المنحة                                                   |
| التشغيل                 | مشروع Node تبني له Docker Image                    | Docker Image رسمية                                                        |
| بيانات الاستخدام        | تستطيع إيقافها                                     | إيقافها في Enterprise فقط                                                 |

والخلاصة العملية: إذا كنت تبدأ مشروعاً جديداً وتريد ترخيصاً مفتوح المصدر دون حدود وفريقك يكتب JavaScript، فابدأ ب Strapi. وإذا كانت عندك قاعدة بيانات قائمة تريد لها لوحة تحرير و API بسرعة، وفريقك صغير يقع ضمن المنحة أو ضمن حدود Core، فDirectus يوفر عليك وقتاً كبيراً، بشرط أن تقبل شروط الترخيص والاتصال بالشركة. وتوجد أدوات أخرى في الفئة نفسها مثل [Payload](https://github.com/payloadcms/payload?ref=arabroot.io) بترخيص MIT، ولم نجربها في هذا المقال.

## تجربة عملية: النموذج نفسه في Strapi وDirectus

الآن سوف نبني النموذج نفسه في الأداتين: مقالات لها عنوان ومعرف ونص وتصنيف وحالة، ومقالان أحدهما منشور والآخر مسودة لخطة إطلاق لم يحن وقتها، ثم نقرأ المحتوى عبر REST وGraphQL، ونجرب صلاحيات الجمهور والتوكن، ونبحث عن الطريق الذي قد تتسرب منه المسودة. وتحتاج التجربة إلى Docker فقط، وكل المنافذ على `127.0.0.1` فلا يصل إليها أحد من خارج جهازك.

### إنشاء مشروع Strapi

Strapi ليس له Docker Image رسمية، لذلك ننشئ المشروع داخل Container فيه Node.js 22 ونحفظه في Volume:

```bash
docker network create lab-devplat-net
docker run -d --name lab-devplat-strapi --network lab-devplat-net \
  -p 127.0.0.1:41337:1337 -v lab-devplat-strapi-data:/srv -w /srv \
  node:22-bookworm-slim sleep infinity

docker exec -e STRAPI_TELEMETRY_DISABLED=true lab-devplat-strapi sh -c \
  'npx -y create-strapi@5.57.0 cms --non-interactive --skip-cloud --no-run \
   --use-npm --js --no-example --no-git-init --no-enable-ab-tests'
```

في الأوامر أعلاه لاحظ أن الخيار `--skip-cloud` يتخطى خطوة تسجيل الدخول إلى خدمة Strapi السحابية، وأن `--non-interactive` يختار SQLite لقاعدة البيانات، وهي مناسبة للتجربة فقط، أما في الإنتاج فاستخدم PostgreSQL.

### النموذج ملفات في المشروع

بدل أن نضغط الأزرار في Content-Type Builder نكتب ملف النموذج مباشرة، وهو الملف نفسه الذي كانت الواجهة ستنتجه. ملف المقال `src/api/article/content-types/article/schema.json`:

```json
{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": { "singularName": "article", "pluralName": "articles", "displayName": "Article" },
  "options": { "draftAndPublish": true },
  "attributes": {
    "title": { "type": "string", "required": true },
    "slug": { "type": "uid", "targetField": "title", "required": true },
    "body": { "type": "richtext" },
    "category": { "type": "relation", "relation": "manyToOne", "target": "api::category.category" }
  }
}
```

وملف التصنيف `src/api/category/content-types/category/schema.json` بحقل `name` فقط ودون مسودات، ولكل نوع ثلاثة ملفات قصيرة تنشئ ال Controller وال Router وال Service الافتراضية، وهذا ملف ال Router مثلاً:

```javascript
const { factories } = require('@strapi/strapi');

module.exports = factories.createCoreRouter('api::article.article');
```

ثم نضيف إضافة GraphQL ونبني لوحة التحكم ونشغل المشروع في وضع الإنتاج وننشئ حساب المدير:

```bash
docker exec lab-devplat-strapi sh -c 'cd /srv/cms && npm install @strapi/plugin-graphql@5.57.0 && NODE_ENV=production npm run build'
docker exec lab-devplat-strapi sh -c 'cd /srv/cms && npx strapi admin:create-user --firstname=Admin --email=admin@example.com --password=Lab-Pass-2026'
docker exec -d -e NODE_ENV=production -e STRAPI_TELEMETRY_DISABLED=true lab-devplat-strapi sh -c 'cd /srv/cms && npm run start > /tmp/strapi.log 2>&1'
```

والمخرج سوف يكون كما يلي (اختصرنا الجدول):

```text
Successfully created new admin
│ Environment        │ production                                       │
│ Version            │ 5.57.0 (node v22.23.3)                           │
│ Plan               │ Community                                        │
│ Database           │ sqlite                                           │
warn: Built-in GraphQL operation limits are unbounded or invalid for: depthLimit, maxLimit. Configure these limits (for example: defaultLimit: 25, maxLimit: 100, depthLimit: 10). Custom Apollo validation rules may independently enforce limits. See https://docs.strapi.io/cms/configurations/plugins.
[2026-10-07 17:43:16.308] info: Strapi started successfully
```

في المخرج أعلاه لاحظ التالي:

- السطر `Plan: Community` يعني النسخة المجانية بترخيص MIT.
- التحذير الخاص ب GraphQL يقول إن عمق الاستعلام وعدد العناصر في الطلب الواحد دون حد، وهذا ما سوف نصلحه بعد قليل.

والصورة التالية من لوحة التحكم بعد إضافة المقالين، وتظهر فيها حالة كل مقال:

![لوحة Content Manager في Strapi تعرض نوع Article فيه مقالان: Install Docker بالتصنيف Docker والحالة Published، وLaunch plan (draft) دون تصنيف والحالة Draft](https://arabroot.io/content/images/2026/10/headless-cms-05-strapi-admin.webp)

لوحة المحتوى في Strapi من تجربتنا

وإذا حاولت تعديل النموذج من لوحة التحكم في وضع الإنتاج تجد الحقول مقفلة كما في الصورة التالية، وإذا أرسلت الطلب مباشرة إلى ال API يرفضه Strapi:

![شاشة Content-Type Builder لنوع Article بحقول title من نوع Text وslug من نوع UID وbody من نوع Rich text Markdown وcategory علاقة manyToOne مع Category، وبجانب كل حقل قفل](https://arabroot.io/content/images/2026/10/headless-cms-06-strapi-builder.webp)

النموذج للقراءة فقط في الإنتاج، والتعديل يتم في بيئة التطوير ثم ينشر

```text
{"data":null,"error":{"status":403,"name":"PolicyError","message":"Content-Type Builder modifications are disabled in production mode. Schema changes can only be made when running with autoReload enabled (strapi develop).","details":{}}}
```

### التوكن والجمهور في Strapi

ننشئ من لوحة التحكم (أو من API المدير كما فعلنا) توكنين: الأول بصلاحية كاملة Full Access لسكربت يضيف المحتوى ولمدة أسبوع فقط، والثاني للقراءة فقط Read-only لسيرفر البناء. ثم نضيف المقالين، الأول منشور والثاني مسودة بالمعامل `status=draft`:

```bash
S=http://127.0.0.1:41337
curl -s -X POST "$S/api/articles" -H "Authorization: Bearer $FULL" -H 'Content-Type: application/json' \
  -d '{"data":{"title":"Install Docker","slug":"install-docker","body":"Steps to install Docker on Ubuntu","category":"em0j0pw523rkfkev5yg8fy5t"}}'
curl -s -X POST "$S/api/articles?status=draft" -H "Authorization: Bearer $FULL" -H 'Content-Type: application/json' \
  -d '{"data":{"title":"Launch plan (draft)","slug":"launch-plan","body":"Not ready yet"}}'
curl -s -w ' HTTP %{http_code}\n' "$S/api/articles"
```

والمخرج سوف يكون كما يلي:

```text
{"data":{"id":2,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","slug":"install-docker","body":"Steps to install Docker on Ubuntu","createdAt":"2026-10-07T17:43:58.004Z","updatedAt":"2026-10-07T17:43:58.004Z","publishedAt":"2026-10-07T17:43:58.027Z"},"meta":{}}
{"data":{"id":3,"documentId":"hugul39h1oh66ecnsqh512l2","title":"Launch plan (draft)","slug":"launch-plan","body":"Not ready yet","createdAt":"2026-10-07T17:43:58.469Z","updatedAt":"2026-10-07T17:43:58.469Z","publishedAt":null},"meta":{}}
{"data":null,"error":{"status":403,"name":"ForbiddenError","message":"Forbidden","details":{}}} HTTP 403
```

في المخرج أعلاه لاحظ أن REST في Strapi ينشر مباشرة عند الإنشاء ما لم تطلب المسودة، كما ينص [توثيق المعامل status](https://docs.strapi.io/cms/api/rest/status?ref=arabroot.io)، وأن الجمهور ممنوع افتراضياً من كل شيء، وهذا جيد. والآن نفتح للجمهور القراءة فقط (`find` و`findOne`) على المقالات والتصنيفات من Settings ثم Users & Permissions ثم Public، ونقرأ:

```bash
curl -sg "$S/api/articles?fields[0]=title&fields[1]=publishedAt"
curl -sg "$S/api/articles?filters[slug][\$eq]=install-docker&populate=category&fields[0]=title"
```

```text
{"data":[{"id":2,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","publishedAt":"2026-10-07T17:43:58.027Z"}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":1}}}
{"data":[{"id":2,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","category":{"id":1,"documentId":"em0j0pw523rkfkev5yg8fy5t","name":"Docker","createdAt":"2026-10-07T17:43:57.488Z","updatedAt":"2026-10-07T17:43:57.488Z","publishedAt":"2026-10-07T17:43:57.475Z"}}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":1}}}
```

الجمهور يرى المقال المنشور فقط، والمسودة مخفية، ويبدو كل شيء صحيحاً. وتوكن سيرفر البناء يستطيع القراءة ولا يستطيع الكتابة:

```text
{"data":null,"error":{"status":403,"name":"ForbiddenError","message":"Forbidden","details":{}}} HTTP 403
```

### نكسرها: الجمهور يطلب المسودات

ماذا يحدث إذا أضاف زائر عادي، دون أي توكن، المعامل نفسه الذي استخدمناه لإنشاء المسودة؟

```bash
curl -sg "$S/api/articles?status=draft&fields[0]=title&fields[1]=publishedAt"
```

```text
{"data":[{"id":1,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","publishedAt":null},{"id":3,"documentId":"hugul39h1oh66ecnsqh512l2","title":"Launch plan (draft)","publishedAt":null}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":2}}}
```

وعند تشغيل الأمر سوف تجد أن خطة الإطلاق التي لم تنشر ظهرت لأي شخص يعرف رابط ال API، والسبب أن صلاحية `find` في Strapi 5 تعني قراءة النوع، والمعامل `status` يختار النسخة، ولا يوجد في إعدادات الصلاحيات خيار يفصل بين قراءة المنشور وقراءة المسودات. والأمر نفسه يحدث عبر GraphQL:

```bash
curl -s -X POST $S/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{ articles(status: DRAFT) { title publishedAt } }"}'
```

```text
{"data":{"articles":[{"title":"Install Docker","publishedAt":null},{"title":"Launch plan (draft)","publishedAt":null}]}}
```

وهذه مشكلة حقيقية لأي شركة تكتب إعلاناً أو سعراً أو نتيجة قبل موعد نشرها، فالمسودة في ال Headless CMS ليست سراً ما لم تحمها بنفسك.

### الإصلاح: حارس واحد يغطي REST وGraphQL

الحل البسيط الذي يخطر على البال هو تعديل Controller المقالات بحيث يفرض النسخة المنشورة، وقد جربناه فأصلح REST فقط، وبقي GraphQL يعيد المسودات، والسبب أن GraphQL في Strapi لا يمر بال Controller وإنما يستدعي خدمة المستندات Document Service مباشرة. لذلك المكان الصحيح هو [Middleware على Document Service](https://docs.strapi.io/cms/api/document-service/middlewares?ref=arabroot.io)، لأن كل قراءة تمر به مهما كان مصدرها. نضعه في `src/index.js`:

```javascript
'use strict';

// Drafts are for the admin panel and for API tokens (build server, preview) only.
// This runs inside the Document Service, so it covers REST and GraphQL alike.
const DRAFT_READERS = ['admin', 'content-api-token'];
const READS = ['findMany', 'findOne', 'findFirst', 'count'];

module.exports = {
  register({ strapi }) {
    strapi.documents.use(async (context, next) => {
      const ctx = strapi.requestContext.get();
      const who = ctx?.state?.auth?.strategy?.name;
      if (ctx && READS.includes(context.action) && !DRAFT_READERS.includes(who)) {
        context.params = { ...context.params, status: 'published' };
      }
      return next();
    });
  },
  bootstrap() {},
};
```

في الكود أعلاه لاحظ التالي:

- `strapi.requestContext.get()` يعطينا الطلب الحالي، و`ctx.state.auth.strategy.name` هو طريقة المصادقة: `admin` للوحة التحكم، و`content-api-token` للتوكن، وقد قرأنا الاسم الأخير من كود Strapi 5.57.0 نفسه، فتأكد منه عند الترقية.
- كل من سواهما، أي الجمهور ومستخدمو التطبيق، يحصل على النسخة المنشورة مهما كتب في `status`.
- الشرط `ctx &&` يترك العمليات التي لا تأتي من طلب HTTP (مثل سكربتات الترحيل) كما هي.

وبعد إعادة التشغيل نعيد الطلبات الأربعة:

```text
--- public REST asks for drafts:
{"data":[{"id":2,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","publishedAt":"2026-10-07T17:43:58.027Z"}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":1}}}
--- public GraphQL asks for drafts:
{"data":{"articles":[{"title":"Install Docker","publishedAt":"2026-10-07T17:43:58.027Z"}]}}
--- build token REST drafts:
{"data":[{"id":1,"documentId":"tvyrxg0pd2iox6ch7xgfowr9","title":"Install Docker","publishedAt":null},{"id":3,"documentId":"hugul39h1oh66ecnsqh512l2","title":"Launch plan (draft)","publishedAt":null}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":2}}}
--- build token GraphQL drafts:
{"data":{"articles":[{"title":"Install Docker","publishedAt":null},{"title":"Launch plan (draft)","publishedAt":null}]}}
--- admin panel still sees the draft:
[('Install Docker', 'published'), ('Launch plan (draft)', 'draft')]
```

الجمهور لم يعد يرى المسودة من REST ولا من GraphQL، وتوكن سيرفر البناء يراها لصفحة المعاينة، ولوحة التحكم تعمل كما هي. وأخيراً نضع حدود GraphQL التي حذرنا منها السجل في `config/plugins.js` إلى جانب ما فيه من إعدادات، بحسب [توثيق الإضافة](https://docs.strapi.io/cms/plugins/graphql?ref=arabroot.io) الذي ينص على أن عمق الاستعلام دون حد ما لم تضعه وأن وضعه يقلل خطر هجمات تعطيل الخدمة DoS:

```javascript
  graphql: {
    config: {
      depthLimit: 7,        // no nesting deeper than 7 levels
      defaultLimit: 25,
      maxLimit: 100,        // nobody gets more than 100 entries in one query
      landingPage: false,   // no GraphQL sandbox page in production
    },
  },
```

وبعد إعادة التشغيل لم يعد التحذير يظهر في السجل.

### Directus: النموذج نفسه عبر ال API

Directus يأتي ك Docker Image رسمية، فنشغله ب SQLite ونعطيه بيانات المدير ورابطه العام:

```bash
docker run -d --name lab-devplat-directus --network lab-devplat-net \
  -p 127.0.0.1:48055:8055 -v lab-devplat-directus-data:/directus/database \
  -e SECRET=lab-secret-change-me-0123456789abcdef -e DB_CLIENT=sqlite3 \
  -e DB_FILENAME=/directus/database/data.db \
  -e ADMIN_EMAIL=admin@example.com -e ADMIN_PASSWORD=Lab-Pass-2026 \
  -e PUBLIC_URL=http://127.0.0.1:48055 -e TELEMETRY=false directus/directus:12.4.1
```

وعند أول دخول إلى لوحة التحكم تسألك نافذة عن مفتاح الترخيص، وتعرض عليك الطبقة الأساسية Core دون مفتاح:

![نافذة Have a license key في Directus 12.4.1 تقول إن الجهات المؤهلة التي إيراداتها أقل من 5 ملايين دولار قد تحصل على Open Innovation Grant، وفيها خياران: I have a license key لخطط OIG وTeam وEnterprise، وI'm using Core plan مجانية دون مفتاح، وزرا Skip وSave](https://arabroot.io/content/images/2026/10/headless-cms-07-directus-license.webp)

أول ما يظهر بعد الدخول في Directus 12: اختيار مفتاح الترخيص أو الطبقة الأساسية

ثم نافذة ثانية تطلب بريد مالك المشروع والموافقة على الترخيص واتفاقية معالجة البيانات، ولاحظ أن Directus يرسل هذا البريد إلى الشركة عند تعيينه كما في [صفحة بيانات الاستخدام](https://directus.com/docs/licensing/telemetry?ref=arabroot.io):

![نافذة You have not set a project owner في Directus تقول إن البريد مطلوب للامتثال لترخيص MSCL-1.0-GPL، وفيها حقل Owner Email وخيار الموافقة على ترخيص Directus MSCL-1.0-GPL واتفاقية معالجة البيانات Data Processing Agreement، وخيار استقبال تحديثات المنتج، وزرا Remind Later وSet Owner](https://arabroot.io/content/images/2026/10/headless-cms-08-directus-owner.webp)

Directus يطلب مالكاً للمشروع للامتثال للترخيص الجديد

بعد ذلك ننشئ النموذج عبر ال API بدل الواجهة، حتى يكون قابلاً للتكرار. نسجل الدخول ونحفظ التوكن، ثم ننشئ مجموعة التصنيفات ومجموعة المقالات بالحقول نفسها، والملف `d-articles.json` كما يلي:

```json
{
  "collection": "articles",
  "schema": {},
  "meta": { "icon": "article", "archive_field": "status", "archive_value": "archived", "unarchive_value": "draft" },
  "fields": [
    { "field": "id", "type": "integer", "schema": { "is_primary_key": true, "has_auto_increment": true }, "meta": { "hidden": true } },
    { "field": "status", "type": "string", "schema": { "default_value": "draft", "is_nullable": false },
      "meta": { "interface": "select-dropdown", "options": { "choices": [ { "text": "Draft", "value": "draft" }, { "text": "Published", "value": "published" }, { "text": "Archived", "value": "archived" } ] } } },
    { "field": "title", "type": "string", "schema": { "is_nullable": false } },
    { "field": "slug", "type": "string", "schema": { "is_unique": true, "is_nullable": false } },
    { "field": "body", "type": "text", "meta": { "interface": "input-rich-text-md" } },
    { "field": "category", "type": "integer", "schema": { "foreign_key_table": "categories", "foreign_key_column": "id" }, "meta": { "interface": "select-dropdown-m2o" } }
  ]
}
```

```bash
D=http://127.0.0.1:48055
T=$(curl -s -X POST $D/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"Lab-Pass-2026"}' | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['access_token'])")
curl -s -X POST $D/collections -H "Authorization: Bearer $T" -H 'Content-Type: application/json' -d @d-categories.json
curl -s -X POST $D/collections -H "Authorization: Bearer $T" -H 'Content-Type: application/json' -d @d-articles.json
curl -s -X POST $D/relations -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
  -d '{"collection":"articles","field":"category","related_collection":"categories"}'
```

لاحظ أن الحالة هنا حقل عادي اسمه `status` وليست نسختين كما في Strapi، فالمسودة في Directus عنصر قيمة حقله `draft`. وبعد إضافة المقالين نفسيهما تظهر لوحة المحتوى كما يلي:

![لوحة Directus تعرض مجموعة Articles فيها عنصران: Install Docker بالحالة published، وLaunch plan (draft) بالحالة draft ونصه Edited by editor1، وفي الشريط الجانبي Articles وCategories](https://arabroot.io/content/images/2026/10/headless-cms-09-directus-studio.webp)

المقالان في Directus، والحالة فيه حقل عادي

### الجمهور يرى المنشور فقط؟ ليس في Core

في Directus تفتح القراءة للجمهور بإضافة صلاحية إلى سياسة Public، والصلاحية تقبل قاعدة على العناصر، فنكتب الطريقة الصحيحة: الجمهور يقرأ المقالات التي حالتها `published` فقط:

```bash
P=abf8a154-5b1c-4a46-ac9c-7300570f4f17   # id of the Public policy from GET /policies
curl -s -X POST $D/permissions -H "Authorization: Bearer $T" -H 'Content-Type: application/json' -d '[
  { "policy": "'$P'", "collection": "articles", "action": "read", "fields": ["id","title","slug","body","category","status"],
    "permissions": { "status": { "_eq": "published" } } },
  { "policy": "'$P'", "collection": "categories", "action": "read", "fields": ["*"], "permissions": {} } ]'
```

```text
{"errors":[{"message":"custom_permission_rules_enabled is a restricted resource.","extensions":{"category":"custom_permission_rules_enabled","code":"RESOURCE_RESTRICTED"}}]}
```

رفض Directus الصلاحية، والسبب أن قواعد الصلاحيات المخصصة من الميزات التي نقلها الإصدار 12 خارج الطبقة الأساسية. وجربنا بعدها تحديد الحقول دون أي قاعدة، فرفضها برسالة الخطأ نفسها، ولم يقبل إلا الصلاحية الكاملة على كل الحقول:

```bash
curl -s -X POST $D/permissions -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
  -d '{"policy":"'$P'","collection":"articles","action":"read","fields":["*"]}'
curl -s "$D/items/articles?fields=id,title,status"
curl -sg "$D/items/articles?fields=id,title,status&filter[status][_eq]=published"
```

```text
{"data":{"id":1,"collection":"articles","action":"read","permissions":null,"validation":null,"presets":null,"fields":["*"],"policy":"abf8a154-5b1c-4a46-ac9c-7300570f4f17"}}
{"data":[{"id":1,"title":"Install Docker","status":"published"},{"id":2,"title":"Launch plan (draft)","status":"draft"}]}
{"data":[{"id":1,"title":"Install Docker","status":"published"}]}
```

في المخرج أعلاه لاحظ أن الجمهور أصبح يقرأ المسودة، وأن إخفاءها بالتصفية `filter` في رابط الطلب لا يحمي شيئاً، لأن الزائر هو من يكتب الرابط ويستطيع حذف التصفية. لذلك في الطبقة الأساسية لا تفتح للجمهور أي مجموعة فيها مسودات، وإنما اجعل الواجهة تقرأ من سيرفرها بتوكن كما سوف نرى الآن، أو احصل على مفتاح المنحة إذا كانت جهتك مؤهلة. وأما القراءة عبر GraphQL فتعمل دون إضافة:

```bash
curl -s -X POST $D/graphql -H 'Content-Type: application/json' \
  -d '{ "query": "{ articles(filter: { status: { _eq: \"published\" } }) { title slug category { name } } }" }'
```

```text
{"data":{"articles":[{"title":"Install Docker","slug":"install-docker","category":{"name":"Docker"}}]}}
```

### الأدوار والمقاعد والتوكن في Directus

ننشئ سياسة Editor تسمح بالدخول إلى لوحة التحكم وإنشاء المقالات وتعديلها دون تغيير النموذج، ثم دوراً Editor، ثم ثلاثة محررين:

```text
editor1:
{"data":{"id":"16247403-d32e-44b5-8a58-aa5c13826e56", … ,"email":"editor1@example.com", …
editor2:
{"data":{"id":"27994669-bcf6-4993-91f2-76aa51f791e0", … ,"email":"editor2@example.com", …
editor3:
{"errors":[{"message":"seats limit exceeded.","extensions":{"category":"seats","code":"LIMIT_EXCEEDED"}}]}
```

المدير ومحرران هم المقاعد الثلاثة في Core، والمحرر الثالث رفض. أما سيرفر البناء فننشئ له مستخدماً للـ API فقط بسياسة قراءة دون دخول إلى لوحة التحكم وبتوكن ثابت، وهذا لا يحسب مقعداً:

```text
--- build token can read:
{"data":[{"id":1,"status":"published"},{"id":2,"status":"draft"}]}
--- build token cannot write:
{"errors":[{"message":"You don't have permission to access collection \"articles\" or it does not exist. Queried in root.","extensions":{"reason":"You don't have permission to access collection \"articles\" or it does not exist. Queried in root.","code":"FORBIDDEN"}}]}
--- editor login + update:
{"data":{"id":2,"status":"draft"}}
--- editor cannot change data model:
{"errors":[{"message":"You don't have permission to access this.","extensions":{"code":"FORBIDDEN"}}]} HTTP 403
```

وأخيراً نأخذ Snapshot للنموذج حتى نحفظه في Git وننقله إلى بيئة أخرى بالأمر `schema apply`:

```bash
docker exec lab-devplat-directus node cli.js schema snapshot --yes /tmp/snapshot.yaml
docker exec lab-devplat-directus head -8 /tmp/snapshot.yaml
```

```text
version: 1
directus: 12.4.1
vendor: sqlite
collections:
  - collection: articles
    meta:
      accountability: all
      archive_app_filter: true
```

### التنظيف

```bash
docker rm -f lab-devplat-strapi lab-devplat-directus
docker volume rm lab-devplat-strapi-data lab-devplat-directus-data
docker network rm lab-devplat-net
```

## أخطاء شائعة

- **الاعتماد على التصفية في رابط الطلب لإخفاء المسودات:** الرابط يكتبه الزائر، والحماية تكون في الصلاحيات أو في Middleware على السيرفر كما رأينا.
- **توكن بصلاحية كاملة في كود الواجهة:** كل من يفتح المتصفح يقرؤه، واجعل التوكن على السيرفر وللقراءة فقط.
- **تعديل النموذج مباشرة في الإنتاج:** النموذج يتغير في بيئة التطوير ثم ينتقل بالنشر أو بملف Snapshot، والسبب أن التعديل اليدوي في الإنتاج لا يمكن مراجعته ولا تكراره، كما شرحنا في [البيئات وGitOps](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/).
- **GraphQL دون حدود:** ضع حداً للعمق ولعدد العناصر قبل أن تفتح ال API للجمهور، وراجع [أمن واجهات API](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-%D9%85%D8%B9-owasp-api-security-top-10/).
- **اختيار الأداة دون قراءة الترخيص الحالي:** ما كان صحيحاً عن ترخيص Directus قبل يونيو 2026 لم يعد صحيحاً، فاقرأ ملف الترخيص في المستودع في يوم اختيارك.
- **SQLite في الإنتاج:** مناسب للتجربة، أما في الإنتاج فاستخدم PostgreSQL وخذ له نسخة احتياطية مع مجلد الملفات المرفوعة Uploads.

## قائمة تحقق قبل أن تفتح ال API

- اطلب المسودات بدون توكن من REST ومن GraphQL، وتأكد أن الرد لا يحتوي إلا المنشور.
- كل توكن له اسم وغرض ومدة، وتوكن الواجهة للقراءة فقط وعلى السيرفر.
- حدود GraphQL للعمق وعدد العناصر مضبوطة، وصفحة التجربة مغلقة في الإنتاج.
- النموذج في Git (ملفات Strapi أو Snapshot في Directus) ويتغير بطلب دمج.
- ال CMS خلف [Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/) بشهادة TLS، ولوحة التحكم `/admin` محمية بالتحقق الثنائي أو بعنوان داخلي.
- قرأت ترخيص الإصدار الذي ستستخدمه، وتعرف ما يرسله السيرفر إلى خارج شبكتك.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- ال Headless CMS يحتفظ بلوحة التحرير وقاعدة البيانات ويقدم المحتوى عبر API لكل الواجهات، وثمنه أنك تبني الواجهة والمعاينة بنفسك، لذلك لا تختره لموقع بسيط.
- نموذج المحتوى تعرفه مرة واحدة فتحصل على الجداول ومسارات REST ومخطط GraphQL، وصلاحيات لوحة التحكم منفصلة عن صلاحيات ال API.
- في Strapi 5 يستطيع الجمهور طلب المسودات بالمعامل `status=draft` إذا كانت له صلاحية القراءة، والإصلاح Middleware على Document Service يغطي REST وGraphQL معاً.
- Strapi بترخيص MIT للنسخة المجانية والنموذج فيه ملفات في المشروع، وDirectus يعمل فوق أي قاعدة بيانات لكنه منذ الإصدار 12 بترخيص MSCL غير مفتوح المصدر، وطبقته المجانية 3 مقاعد و25 مجموعة دون قواعد صلاحيات مخصصة.
- وفي مقالات قادمة بإذن الله من مرحلة [منصات للمطورين](https://arabroot.io/start/#stage-devplatform) نتحدث عن بيئات التطوير في المتصفح، وعن ال Feature Flags وأدواتها، وعن بوابات API.

[![](https://arabroot.io/content/images/2026/10/api-design-cover-cover.webp)دليلكيف تصمم API لا تضطر إلى كسره بعد سنةمبادئ تصميم API يتطور دون أن يكسر التطبيقات التي تعتمد عليه، وكيف تختار الشكل المناسب لنظامك.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)[![](https://arabroot.io/content/images/2026/10/open-source-licenses-cover-cover.webp)دليلما هي البرمجيات مفتوحة المصدر؟ القصة وأشهر الرخصكيف بدأت البرمجيات الحرة والمفتوحة، وما الفرق بين الحر والمجاني، وكيف تعرف ما تسمح به رخصة أي مشروع قبل أن تبني عليه.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/authorization-cover-cover.webp)دليلAuthorization: كيف تجمع قرارات الصلاحيات في مكان واحدلماذا تتحول شروط الصلاحيات المتفرقة في الكود إلى ثغرات، وكيف تنقل القرار إلى محرك سياسات واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/)[![](https://arabroot.io/content/images/2026/10/environments-gitops-cover-cover.webp)دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة Strapi 5.57.0 مع إضافة GraphQL وDirectus 12.4.1 في Docker، والتحقق من ترخيص كل منهما من المستودع والتوثيق الرسمي.