> ## 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.

# تعدد المستأجرين Multi-Tenancy في SaaS للمبتدئين
- URL: https://arabroot.io/articles/تعدد-المستاجرين-multi-tenancy-في-saas-للمبتدئين/
- Published: 2026-10-05T20:30:00.000Z
- Updated: 2026-10-14T07:41:18.000Z
- Description: كيف يخدم تطبيق SaaS واحد مئات العملاء دون أن يرى أحدهم بيانات غيره، وأي نموذج عزل يناسب تطبيقك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, DevOps وCI/CD, قواعد البيانات, الاستضافة الذاتية

لنفرض أنك بنيت تطبيقاً للفواتير لشركة واحدة، ونجح التطبيق، ثم طلبت شركة ثانية نفس التطبيق، ثم ثالثة وعاشرة. أول حل يخطر على البال أن تنسخ السيرفر وقاعدة البيانات لكل شركة، وهذا يعمل مع خمس شركات، ولكن مع مئة شركة تجد نفسك تحدث مئة نسخة، وتراقب مئة قاعدة بيانات، وتدفع ثمن سيرفرات لا تعمل شيئاً أغلب اليوم، وكل نسخة على إصدار مختلف لأن التحديث لم يصلها بعد. والحل الذي وصلت إليه كل شركات البرمجيات كخدمة SaaS تقريباً هو أن يخدم تطبيق واحد كل العملاء معاً، وهذا ما يسمى تعدد المستأجرين Multi-Tenancy. وقد ذكرنا في [المدخل إلى تصميم الأنظمة](https://arabroot.io/articles/%D9%85%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D9%85%D9%86-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86/) كيف يكبر النظام من سيرفر واحد، وفي [قصة Stack Overflow](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A8%D9%86%D9%89-stack-overflow-%D9%85%D9%88%D9%82%D8%B9%D9%87-%D8%A8%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA-%D9%82%D9%84%D9%8A%D9%84%D8%A9/#request-path) رأينا تطبيقاً واحداً متعدد المستأجرين يخدم كل مواقع Stack Exchange ويعرف الموقع المطلوب من اسم النطاق. وفي هذا المقال نشرح الفكرة من أساسها: ما هو المستأجر Tenant، وكيف تعزل بيانات كل مستأجر عن غيره، ولماذا يكفي استعلام واحد نسي شرطاً صغيراً حتى يرى عميل فواتير عميل آخر.

هذا المقال جزء من سلسلة من مقالين:

- **تعدد المستأجرين Multi-Tenancy في SaaS للمبتدئين: المفاهيم والبنية** (الموضوع الحالي).
- [بناء تطبيق SaaS متعدد المستأجرين عملياً](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/): تطبيق سياسات الصفوف Row-Level Security في PostgreSQL، وأتمتة إنشاء المستأجر Tenant Provisioning، وترحيل الجداول على كل المستأجرين، والنسخ الاحتياطي والتصدير والحذف لكل مستأجر، ونقل مستأجر بين قواعد البيانات، مع مختبر كامل.

وسوف نناقش في هذا المقال ما يلي:

- ما هو المستأجر Tenant، والفرق بين مستأجر واحد لكل نسخة Single-tenant ونسخة واحدة لكل المستأجرين Multi-tenant، ولماذا تختار شركات SaaS الثانية، وما هو الخطر الحقيقي فيها.
- نماذج العزل الثلاثة بمصطلحات AWS: المعزول Silo والمشترك Pool والهجين Bridge، وكيف تختار نموذجاً لكل طبقة، وكيف ترتبط بالباقات Tiers.
- ثلاثة خيارات لبيانات كل مستأجر Tenant في PostgreSQL: قاعدة بيانات لكل مستأجر، أو مخطط Schema لكل مستأجر، أو جداول مشتركة مع عمود `tenant_id` وسياسات الصفوف Row-Level Security حارساً، ومقارنة صادقة بينها.
- سياق المستأجر Tenant Context: كيف يعرف كل طلب مستأجره من النطاق الفرعي أو نطاق العميل أو التوكن، وكيف يحمل هذه المعلومة إلى قاعدة البيانات وال Cache والسجلات والطوابير.
- الهوية والصلاحيات: دخول يعرف المستأجر Tenant، ومستخدم في أكثر من مستأجر، ودخول موحد SSO وتعيين الحسابات SCIM لكل عميل.
- الجار المزعج Noisy Neighbor والعدل بين المستأجرين Tenants: الحدود Quotas والطوابير والمراقبة والفوترة لكل مستأجر.
- إعدادات كل مستأجر Tenant دون نسخة خاصة من الكود، ودورة حياة المستأجر من التسجيل إلى الحذف.
- كيف فعلتها Notion وSlack وFigma، والمشاريع مفتوحة المصدر التي تطبق هذه الأنماط، ودليل قرار، وتجربة صغيرة تريك التسرب بعينك.

## ما هو المستأجر Tenant؟

المستأجر Tenant في نظام SaaS هو العميل الذي اشترك في الخدمة، وغالباً يكون مؤسسة كاملة: شركة أو مستشفى أو مدرسة، لها موظفوها وإعداداتها وبياناتها وفاتورتها، وليس مستخدماً واحداً. لذلك عندما تشترك شركة acme في تطبيق الفواتير فهي مستأجر واحد، وموظفوها العشرون مستخدمون Users داخل هذا المستأجر، وكل واحد منهم يرى بيانات acme وحدها. والتسمية تأتي من عمارة الشقق: بناية واحدة يتشارك سكانها المصعد والكهرباء والماء، ولكل شقة باب مقفل لا يفتحه إلا أهلها، فالبناية هي التطبيق، والشقق هي المستأجرون، والباب المقفل هو العزل Isolation الذي سوف يأخذ أغلب هذا المقال. وفي بعض المنتجات يسمى المستأجر باسم آخر، مثل مساحة العمل Workspace في Slack وNotion، أو المؤسسة Organization في GitHub، أو الحساب Account، والفكرة نفسها.

وهناك طريقتان لخدمة أكثر من عميل:

- **مستأجر Tenant واحد لكل نسخة Single-tenant:** لكل عميل نسخته من التطبيق وقاعدة بياناته، وأحياناً سيرفره، فالعزل كامل، ولكنك تدير عدد النسخ بعدد العملاء.
- **نسخة واحدة لكل المستأجرين Multi-tenant:** تطبيق واحد وقاعدة بيانات واحدة أو عدد قليل منها يخدم كل العملاء، وكل سطر في قاعدة البيانات يعرف لمن ينتمي.

الصورة التالية تبين الطريقتين بنفس العملاء الثلاثة:

![على اليمين ثلاث شركات acme وglobex وinitech لكل واحدة تطبيقها بإصدار مختلف v2.3 وv2.1 وv1.9 وقاعدة بياناتها الخاصة، وعلى اليسار نفس الشركات الثلاث تتصل بتطبيق واحد إصداره v2.3 وجدول واحد فيه عمود tenant_id وصفوف ملونة لكل شركة، مع درع أحمر يحذر من أن استعلاماً ينسى tenant_id يكشف بيانات عميل لآخر](https://arabroot.io/content/images/2026/10/multi-tenancy-01-single-vs-multi.webp)

نسخة لكل عميل تعطيك عزلاً كاملاً وتكلفة تتضاعف، والنسخة المشتركة توفر الكثير ولكن العزل يصبح مسؤولية الكود

### لماذا تختار شركات SaaS تعدد المستأجرين Multi-Tenancy؟

الأسباب الثلاثة الأساسية عملية جداً:

- **التكلفة Cost:** أغلب العملاء صغار ويستخدمون التطبيق ساعات قليلة في اليوم، فإذا أخذ كل واحد سيرفراً وقاعدة بيانات فأنت تدفع ثمن موارد خاملة، أما في النسخة المشتركة فيملأ استخدام عميل وقت فراغ عميل آخر، وكما تقول [إرشادات Azure لبيانات الأنظمة متعددة المستأجرين Tenants](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) فكلما زادت كثافة المستأجرين على البنية قلت تكلفة تشغيلها.
- **التشغيل Operations:** نسخة واحدة تراقبها وتنسخها احتياطياً وتصلح ثغراتها مرة واحدة، بدل مئة نسخة تنسى إحداها.
- **إصدار واحد للجميع:** كل عميل يحصل على الميزة الجديدة وإصلاح الثغرة في نفس اليوم، وهذا ما تسميه [مبادئ AWS SaaS Lens](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) بيئة واحدة يعمل عليها كل العملاء دون نسخ خاصة بعميل معين One-off Versions، لأن النسخ الخاصة تقتل سرعة الشركة في التطوير.

وفي المقابل يوجد خطر واحد كبير يجب أن تراه بوضوح من اليوم الأول: أن يرى مستأجر Tenant بيانات مستأجر آخر. فالبيانات كلها في نفس الجداول، والفاصل بينها شرط `WHERE tenant_id = ...` في كل استعلام، ويكفي أن ينساه مطور مرة واحدة في تقرير جديد. ووثيقة [عزل المستأجرين Tenant Isolation في AWS SaaS Lens](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/tenant-isolation.html?ref=arabroot.io) تصف عبور هذا الحد بأي شكل بأنه حدث كبير قد لا تتعافى منه شركة SaaS، والسبب أن العميل يغفر بطء الصفحة ولا يغفر أن يرى منافسه فواتيره. لذلك كل ما في بقية المقال هو إجابة على سؤال واحد: كيف تحصل على وفر النسخة المشتركة دون أن تدفع ثمنه من عزل البيانات؟

## نماذج العزل الثلاثة: المعزول Silo والمشترك Pool والهجين Bridge

تستخدم وثيقة [AWS SaaS Lens](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/silo-pool-and-bridge-models.html?ref=arabroot.io) ثلاثة مصطلحات واضحة لنماذج العزل، وسوف نستخدمها في بقية المقال:

- **النموذج المعزول Silo:** موارد مخصصة لكل مستأجر Tenant، مثل قاعدة بيانات خاصة به، أو بنية كاملة مستقلة. ولاحظ النقطة المهمة التي تذكرها الوثيقة: حتى في هذا النموذج تبقى الهوية وتسجيل العملاء Onboarding والتشغيل مشتركة بين كل المستأجرين ومدارة من مكان واحد، وهذا ما يجعله SaaS وليس خدمة مدارة Managed Service يشغل فيها كل عميل نسخة مختلفة بإجراءات مختلفة.
- **النموذج المشترك Pool:** المستأجرون Tenants يتشاركون نفس الموارد، الحوسبة والتخزين والطوابير، وهذا هو المعنى الكلاسيكي لتعدد المستأجرين، ومنه يأتي الوفر في التكلفة والإدارة.
- **النموذج الهجين Bridge:** الاعتراف بأن أغلب الأنظمة ليست معزولة كلها ولا مشتركة كلها، فبعض الخدمات معزولة وبعضها مشترك، والوثيقة تذكر أن طبيعة بيانات خدمة معينة من ناحية الأنظمة واللوائح أو تأثير الجار المزعج قد تدفعها إلى العزل، بينما تبقى خدمة أخرى مشتركة لأن تكلفتها ونمط استخدامها يناسبان ذلك.

الصورة التالية تبين النماذج الثلاثة على طبقات الحوسبة والبيانات والشبكة:

![ثلاثة أعمدة: Silo فيه لكل مستأجر تطبيق وقاعدة بيانات وشبكة VPC خاصة، وPool فيه تطبيق واحد وقاعدة بيانات واحدة صفوفها تحمل tenant_id وشبكة مشتركة، وBridge فيه تطبيق مشترك وقاعدة مشتركة للمستأجرين الصغار وقاعدة خاصة للمستأجر الكبير، وفوقها جميعاً شريط طبقة التحكم المشتركة، وتحتها سهم من العزل الأقوى والتكلفة الأعلى إلى التكلفة الأقل والعزل في الكود](https://arabroot.io/content/images/2026/10/multi-tenancy-02-silo-pool-bridge.webp)

النماذج الثلاثة على كل طبقة، وطبقة التحكم مشتركة في الحالات كلها

### العزل في كل طبقة: الحوسبة Compute والبيانات Data والشبكة Network والهوية Identity

الخطأ الشائع أن تسأل «هل نظامنا Silo أم Pool؟» كأنه قرار واحد، والصحيح أن تسأل السؤال لكل طبقة على حدة، وهذا ما تنص عليه [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) عندما تطلب أن تقسم كل خدمة حسب حملها وحاجتها للعزل، وفيما يلي ما يعنيه العزل في كل طبقة:

- **الحوسبة Compute:** في النموذج المشترك تخدم نفس العمليات طلبات كل المستأجرين، وفي المعزول يكون لكل مستأجر Tenant عملياته أو Containers خاصة به. وفي Kubernetes تشرح [الوثائق الرسمية لتعدد المستأجرين](https://kubernetes.io/docs/concepts/security/multi-tenancy/?ref=arabroot.io) أن العزل طيف وليس خياراً ثنائياً: أبسطه Namespace لكل مستأجر مع الصلاحيات RBAC والحصص Resource Quotas وسياسات الشبكة Network Policies، وأقوى منه لوحة تحكم افتراضية لكل مستأجر Virtual Control Plane مثل [vCluster](https://github.com/loft-sh/vcluster?ref=arabroot.io) (بالرخصة Apache-2.0)، وأقوى منها Cluster كامل لكل مستأجر.
- **البيانات Data:** قاعدة بيانات لكل مستأجر Tenant، أو مخطط Schema لكل مستأجر، أو جداول مشتركة، وهذه الطبقة هي الأهم ولها [قسم كامل](#data-isolation) بعد قليل.
- **الشبكة Network:** في المعزول قد يكون لكل مستأجر Tenant شبكته الخاصة VPC أو على الأقل سياسات شبكة تمنع خدماته من الوصول إلى خدمات غيره، وفي المشترك تكون الشبكة واحدة والفصل في التطبيق.
- **الهوية Identity:** كل طلب يجب أن يحمل هوية المستخدم ومستأجره Tenant معاً، وهذه الطبقة مشتركة تقريباً في كل الأنظمة لأن تسجيل الدخول والفوترة وتسجيل العملاء تدار من مكان واحد، وسوف نشرحها في [قسم الهوية](#identity).

وهذا يعني أن نظاماً واحداً قد يشغل كل المستأجرين Tenants على نفس التطبيق Pool في الحوسبة، ويعطي العميل الحكومي قاعدة بيانات خاصة Silo في البيانات، وهذا بالضبط ما يسمى Bridge.

📌

في أغسطس 2021 نشر باحثو Wiz تفاصيل ثغرة سموها [ChaosDB](https://www.wiz.io/blog/chaosdb-how-we-hacked-thousands-of-azure-customers-databases?ref=arabroot.io) في ميزة Jupyter Notebook داخل خدمة Azure Cosmos DB، وهي خدمة مشتركة بين آلاف العملاء، وكتبوا أنها أعطتهم وصولاً كاملاً إلى حسابات وقواعد بيانات عدة آلاف من عملاء Azure عبر مفاتيحهم الأساسية Primary Keys التي تسمح بالقراءة والكتابة والحذف، وأن Microsoft عطلت الميزة خلال 48 ساعة من البلاغ وطلبت من العملاء تغيير مفاتيحهم. والدرس هنا أن العزل في طبقة البيانات لا يكفي إذا كانت ميزة إضافية في طبقة الحوسبة المشتركة تستطيع عبور الحد بين المستأجرين، لذلك افحص كل ميزة جديدة تعمل على موارد مشتركة بسؤال واحد: هل تستطيع الوصول إلى شيء لا يخص المستأجر الحالي؟

### مستوى العزل جزء مما يدفعه العميل في الباقات Tiering

بما أن الاختيار يكون لكل طبقة، فمن الطبيعي أن يكون أيضاً لكل باقة، وتذكر [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) أن بعض الخدمات تعمل بنموذج Silo لعملاء الباقة الأعلى Premium Tier كجزء من قيمة هذه الباقة، وتذكر [إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) مثالاً مشابهاً: قاعدة بيانات مشتركة لأغلب المستأجرين Tenants، ونسخة مخصصة لمن يدفع أكثر أو له متطلبات غير عادية. والصورة التالية تبين ثلاث باقات على نفس الكود:

![شريط طبقة التحكم في الأعلى مع أعلام ميزات sso مفعلة وaudit_export معطلة، وتحته ثلاث باقات: Basic فيها مئات المستأجرين الصغار على نسختين مشتركتين من التطبيق وقاعدة بيانات واحدة، وPro فيها ثلاثة مستأجرين على تطبيق مشترك وقاعدة أقل ازدحاماً shard 2، وEnterprise فيها مستأجر واحد بنسخ تطبيق وقاعدة بيانات خاصة في منطقة KSA، وتحت كل باقة حدودها كمثال](https://arabroot.io/content/images/2026/10/multi-tenancy-07-tiering.webp)

الباقة تغير الحدود ومكان التشغيل، والكود نفسه واحد للجميع

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

- الكود واحد، وطبقة التحكم واحدة تنشر الإصدار على كل الباقات، والفرق كله في الإعدادات ومكان التشغيل، وإذا احتجت إلى `if tenant == 'acme'` في الكود فقد بدأت تبني نسخة خاصة، وسوف نعود لهذا في [قسم الإعدادات](#configuration).
- العميل في باقة Basic لا يحتاج قاعدة بيانات خاصة ولن يدفع ثمنها، والعميل الذي يفرض عليه نظام أو عقد أن تبقى بياناته في المملكة أو منفصلة فعلياً يدفع ثمن ذلك في باقة أعلى.
- الأرقام في الصورة مثال فقط، والذي تحدده أنت هو أي الحدود تهم منتجك.

## أين تضع بيانات كل مستأجر Tenant؟

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

![ثلاثة أعمدة: Database per tenant فيه تطبيق بثلاثة مجمعات اتصالات إلى ثلاث قواعد بيانات على سيرفر PostgreSQL، وSchema per tenant فيه تطبيق يغير search_path في قاعدة واحدة فيها مخططات acme وglobex وinitech بنفس الجداول، وShared tables فيه تطبيق بمجمع واحد وجدول فيه عمود tenant_id ودرع RLS، وتحتها جدول يقارن قوة العزل والتكلفة والاتصالات والترحيل واستعادة مستأجر واحد والعدد المناسب](https://arabroot.io/content/images/2026/10/multi-tenancy-03-data-options.webp)

ثلاثة خيارات للبيانات، وكل واحد يدفع ثمناً في مكان مختلف

### قاعدة بيانات لكل مستأجر Database per Tenant

هذا أول حل يخطر على البال لأنه لا يحتاج أي تغيير في الكود: كل مستأجر Tenant له قاعدة بيانات باسمه، والتطبيق يختار الاتصال المناسب حسب المستأجر، وبالتالي لا يمكن لاستعلام نسي شرطاً أن يرى بيانات غيره لأن البيانات في مكان آخر أصلاً. ومزاياه حقيقية: العزل قوي، واستعادة مستأجر واحد من نسخة احتياطية تعني استعادة قاعدة واحدة، ونقل عميل إلى سيرفر في منطقة أخرى سهل، ويمكن أن تعطي كل عميل مفتاح تشفير خاصاً به.

ولكن قد يتساءل البعض: إذا كان بهذه القوة فلماذا لا يستخدمه الجميع؟ والإجابة أن ثمنه يظهر مع العدد:

- **تضاعف الاتصالات Connection Multiplication:** كل قاعدة بيانات تحتاج مجمع اتصالات Connection Pool خاصاً بها، لأن الاتصال في PostgreSQL مرتبط بقاعدة واحدة، فمجمع صغير من عشرة اتصالات في كل نسخة من التطبيق يصبح مع ثلاثمئة مستأجر Tenant وثلاث نسخ تسعة آلاف اتصال محتمل، وقد شرحنا في [مقال تجميع الاتصالات](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#many-instances) لماذا يرفض PostgreSQL الاتصالات بعد حد معين ولماذا لا يحل رفع `max_connections` المشكلة.
- **الترحيل Migrations:** كل تغيير في الجداول يجب أن ينفذ على كل القواعد، وإذا فشل على القاعدة رقم 173 فأصبح عندك مستأجرون Tenants على نسخة جداول مختلفة، والتطبيق يجب أن يتعامل مع ذلك.
- **التكلفة والتشغيل:** المراقبة والنسخ الاحتياطي والتحديث لكل قاعدة، وتنبه [إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) إلى أن إنشاء قاعدة لكل مستأجر Tenant دون أتمتة يصبح تعقيداً لا يحتمل.
- **التقارير الشاملة:** سؤال بسيط مثل «كم فاتورة أصدر كل العملاء هذا الشهر؟» يصبح استعلاماً على مئات القواعد ثم تجميع النتائج.

لذلك هذا الخيار مناسب لعدد قليل من العملاء الكبار، أو لعميل واحد يفرض عليه نظام أو عقد فصلاً فعلياً، وغير مناسب إطلاقاً كنقطة بداية لمنتج يتوقع مئات العملاء الصغار.

### مخطط لكل مستأجر Schema per Tenant

المخطط Schema في PostgreSQL مجلد داخل قاعدة البيانات يحمل جداوله، فتنشئ مخططاً لكل مستأجر Tenant فيه نفس الجداول، `acme.invoices` و`globex.invoices`، والتطبيق يختار المخطط في بداية كل طلب بتغيير [search\_path](https://www.postgresql.org/docs/18/ddl-schemas.html?ref=arabroot.io)، فيكتب الكود `SELECT * FROM invoices` دون أن يذكر المستأجر. وهذا هو النمط الذي تطبقه مكتبة [django-tenants](https://github.com/django-tenants/django-tenants?ref=arabroot.io) لإطار Django، و[apartment](https://github.com/rails-on-services/apartment?ref=arabroot.io) لإطار Rails.

ومزاياه أنه وسط بين الخيارين: قاعدة واحدة واتصالات أقل، ونسخة احتياطية لمستأجر Tenant واحد ممكنة بالأمر `pg_dump -n acme`، وعزل أوضح من عمود في جدول. وثمنه:

- الترحيل Migrations ما زال يتكرر بعدد المخططات.
- العدد له حد عملي، فوثائق [Citus](https://docs.citusdata.com/en/stable/get%5Fstarted/concepts.html?ref=arabroot.io) تضع التوزيع بالمخطط Schema-based Sharding في نطاق من مستأجر Tenant واحد إلى عشرة آلاف مستأجر في ال Cluster، مقابل مليون وأكثر للتوزيع بالصفوف Row-based Sharding.
- تغيير `search_path` إعداد على مستوى الجلسة، وإذا كان أمام قاعدة البيانات PgBouncer بوضع transaction فالجلسة لا تبقى لنفس الكلاينت، وقد شرحنا في [قسم ما ينكسر في وضع transaction](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#transaction-mode-breaks) لماذا يتسرب إعداد `SET` إلى طلب آخر، وهنا معنى التسرب أن طلب globex قد ينفذ على مخطط acme، لذلك استخدم `SET LOCAL` داخل المعاملة.

### جداول مشتركة Shared Tables مع عمود tenant\_id

الخيار الثالث أن تكون الجداول واحدة للجميع، وكل جدول فيه عمود `tenant_id`، وكل استعلام يشترط هذا العمود. وهو الخيار الأعلى كثافة والأقل تكلفة، وترحيل الجداول يحدث مرة واحدة، ومجمع الاتصالات واحد، وهو ما تقوم عليه أنظمة كبيرة جداً كما سوف ترى في [قسم الأمثلة الواقعية](#real-world). وثمنه أن العزل أصبح مسؤولية كل سطر في الكود، وهنا ثلاث قواعد تقلل الخطر قبل أن نصل إلى الحارس:

- **اجعل `tenant_id` جزءاً من المفتاح الأساسي Primary Key والمفاتيح الأجنبية Foreign Keys:** مثل `PRIMARY KEY (tenant_id, id)`، وفاتورة تشير إلى عميل عبر `FOREIGN KEY (tenant_id, customer_id)`، والسبب أن قاعدة البيانات نفسها تمنع فاتورة acme من أن تشير إلى عميل يخص globex، وهذا أيضاً ما تشترطه [وثائق Citus](https://docs.citusdata.com/en/stable/use%5Fcases/multi%5Ftenant.html?ref=arabroot.io) للمفاتيح عندما توزع الجداول حسب المستأجر Tenant، فالقرار يخدمك مرتين. وقد شرحنا المفاتيح الأساسية والأجنبية في [مقال تصميم قواعد البيانات](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/#keys).
- **القيود الفريدة Unique Constraints تكون داخل المستأجر Tenant:** البريد الإلكتروني فريد داخل acme وليس في الجدول كله، أي `UNIQUE (tenant_id, email)`، وإلا منعت عميلاً من إضافة موظف لأن عميلاً آخر أضافه قبله، وكشفت له بذلك أن هذا البريد موجود عندك.
- **لا تعتمد على أن المعرف لا يمكن تخمينه:** الطلب `GET /invoices/1` يجب أن يفحص أن الفاتورة رقم 1 تخص مستأجر Tenant المستخدم، وسوف ترى في [التجربة](#lab) كيف يعيد الاستعلام فاتورة acme لمستخدم من globex عندما يبحث بالرقم وحده، وهذه الثغرة لها اسم مشهور هو الوصول المباشر غير الآمن للكائنات IDOR، وقد شرحنا الفحص الصحيح في [مقال الصلاحيات المركزية](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/#where-checks).

### الحارس الأخير: سياسات الصفوف Row-Level Security

القواعد السابقة تعتمد كلها على أن المطور لم ينس، والحل الأنسب أن تضيف حارساً داخل قاعدة البيانات نفسها لا يعتمد على ذاكرة المطور، وهو سياسات الصفوف Row-Level Security واختصاراً RLS في PostgreSQL: تكتب على الجدول سياسة Policy تقول إن الصف يظهر فقط إذا كان `tenant_id` فيه يساوي المستأجر Tenant الحالي، والتطبيق يخبر قاعدة البيانات بالمستأجر الحالي في بداية كل معاملة، مثل `SET LOCAL app.tenant_id = 'acme'`، وبعدها حتى الاستعلام الذي نسي الشرط لا يرى إلا صفوف acme. وتذكر [وثائق PostgreSQL](https://www.postgresql.org/docs/18/ddl-rowsecurity.html?ref=arabroot.io) ثلاث نقاط يجب أن تعرفها من الآن:

- إذا فعلت RLS على جدول ولم توجد سياسة، فالافتراضي هو المنع Default Deny، أي لا يظهر أي صف، وهذا هو السلوك الآمن.
- المستخدمون الخارقون Superusers والأدوار التي لها الخاصية `BYPASSRLS` يتجاوزون السياسات دائماً، ومالك الجدول Table Owner يتجاوزها أيضاً إلا إذا فعلت `FORCE ROW LEVEL SECURITY`، لذلك لا يتصل التطبيق بقاعدة البيانات بالمستخدم الذي أنشأ الجداول.
- فحوص سلامة البيانات مثل القيود الفريدة والمفاتيح الأجنبية تتجاوز السياسات دائماً، وكذلك العمليات على الجدول كله مثل `TRUNCATE`.

وكن صادقاً مع نفسك في الثمن: [إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) تذكر أن RLS يحتاج أن تحمل هوية المستخدم والمستأجر Tenant إلى قاعدة البيانات مع كل استعلام، وأن تصميمه واختباره وصيانته معقد، وأن كثيراً من الأنظمة لا تستخدمه لهذا السبب، و[إرشاداتها الخاصة بـ PostgreSQL](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/service/postgresql?ref=arabroot.io) تنبه إلى أثره على الأداء والحاجة إلى فهارس مناسبة. ومع ذلك فهو في رأينا أرخص من حادثة تسرب واحدة، بشرط أن يكون حارساً إضافياً وليس بديلاً عن الشرط في الكود. وسوف ترى في [التجربة](#lab-rls) مثالاً صغيراً عليه، أما التطبيق الكامل مع الأدوار والمجمع والاختبارات فهو موضوع [المقال الثاني في السلسلة](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/).

### مقارنة الخيارات الثلاثة لعزل البيانات Data Isolation

| الخيار                                      | نقاط القوة                                                                                               | نقاط الضعف                                                                                            | يناسب                                                            |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| قاعدة بيانات لكل مستأجر Database per Tenant | أقوى عزل، واستعادة ونقل وتشفير لكل مستأجر Tenant، ولا يتأثر مستأجر بأقفال غيره                           | مجمع اتصالات لكل قاعدة، وترحيل بعدد القواعد، وتكلفة أعلى، وتقارير شاملة صعبة                          | عدد قليل من العملاء الكبار أو الخاضعين لأنظمة تفرض الفصل         |
| مخطط لكل مستأجر Schema per Tenant           | قاعدة واحدة، ونسخة احتياطية لمستأجر Tenant واحد بالأمر pg\_dump -n، والكود لا يذكر المستأجر في الاستعلام | ترحيل بعدد المخططات، وحد عملي للعدد، وsearch\_path يحتاج حذراً مع PgBouncer                           | عشرات إلى آلاف المستأجرين Tenants بجداول متشابهة                 |
| جداول مشتركة مع tenant\_id                  | أعلى كثافة وأقل تكلفة، وترحيل مرة واحدة، ومجمع واحد، وتقارير شاملة سهلة                                  | العزل في كل استعلام، والجار المزعج أقرب، واستعادة مستأجر Tenant واحد تعني استخراج صفوفه من نسخة كاملة | أغلب منتجات SaaS في بدايتها، وآلاف إلى ملايين المستأجرين Tenants |

ولاحظ أن الاستعادة في الخيار الثالث لها حل معروف ذكرته [إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io): تستعيد النسخة الاحتياطية على سيرفر منفصل ثم تستخرج صفوف المستأجر Tenant المطلوب، وهذا يعمل ولكنه يحتاج أداة جاهزة قبل أن يطلبه العميل، وسوف نبنيها في المقال الثاني. وهناك خيار رابع يذكره نفس المرجع على أنه نمط يجب تجنبه، وهو جدول لكل مستأجر داخل قاعدة واحدة مثل `invoices_acme`، لأنه يجمع عيوب الخيارات كلها ولا يكبر.

### بيانات المستأجر Tenant خارج قاعدة البيانات

البيانات ليست في PostgreSQL وحدها، وكل مخزن آخر يحتاج نفس السؤال: الملفات في التخزين الكائني Object Storage تكون تحت مسار يبدأ بالمستأجر Tenant مثل `tenants/acme/invoices/`، وروابط التنزيل المؤقتة تولد بعد فحص أن الملف يخص المستأجر، وفهرس البحث Search Index إما مشترك مع حقل `tenant_id` يدخل في كل بحث كشرط، أو فهرس لكل مستأجر، والطوابير والرسائل تحمل المستأجر داخل الرسالة نفسها. وأخطر هذه المخازن هو ال Cache، وله [قسم خاص](#cache-key) بعد قليل.

## كيف يعرف الطلب مستأجره؟ سياق المستأجر Tenant Context

كل ما سبق يفترض أن التطبيق يعرف المستأجر الحالي، والسؤال الآن من أين يعرفه. الحل البسيط أن يرسل الكلاينت المستأجر في الطلب، مثل حقل `tenant_id` في جسم الطلب أو ترويسة `X-Tenant-ID`، ثم يثق التطبيق به، وهذا الحل غير مناسب إطلاقاً، والسبب أن أي مستخدم يستطيع تغيير القيمة في الطلب فيصبح فجأة في مستأجر آخر. والقاعدة الصحيحة كما تصوغها [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) أن تربط هوية المستخدم بهوية المستأجر Bind User Identity to Tenant Identity عند تسجيل الدخول، فيحمل كل طلب هوية واحدة تعرف المستخدم ومستأجره، ثم تمر هذه الهوية على كل الطبقات. والصورة التالية تبين الطريق من المتصفح إلى كل طبقة:

![على اليمين أربعة مصادر للمستأجر: نطاق فرعي acme.app.example مع DNS وشهادة Wildcard، ونطاق العميل portal.acme.com مع On-demand TLS، وحقل org_id في التوكن، والمسار أو الترويسة بخط متقطع يجب فحصها، ثم خطوات مرقمة: 1 المستخدم يرسل الطلب إلى ال Reverse Proxy، 2 طبقة Tenant middleware تحول اسم النطاق إلى مستأجر وتفحص أن المستخدم عضو فيه، 3 سياق tenant = acme لهذا الطلب، 4 السياق يذهب إلى قاعدة البيانات مع SET LOCAL وRLS، وإلى مفتاح ال Cache t:acme:stats:unpaid بدل stats:unpaid المتسرب، وإلى السجلات tenant=acme، وإلى رسالة الطابور التي يقرؤها العامل](https://arabroot.io/content/images/2026/10/multi-tenancy-04-tenant-context.webp)

السياق يحدد مرة واحدة في بداية الطلب، ثم يذهب مع الطلب إلى كل طبقة

### النطاق الفرعي Subdomain لكل مستأجر Tenant

أشهر طريقة أن يكون لكل مستأجر Tenant نطاق فرعي، مثل `acme.app.example` و`globex.app.example`، فيعرف التطبيق المستأجر من ترويسة `Host`، ويعرف المستخدم من عنوان المتصفح أين هو. ولكي لا تضيف سجلاً في DNS مع كل عميل جديد تستخدم سجلاً عاماً `*.app.example`، وقد شرحنا في [قسم السجل العام Wildcard](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#wildcard) كيف يجيب عن أي اسم تحت النطاق. وكذلك الشهادة: شهادة واحدة شاملة Wildcard Certificate لـ `*.app.example`، ولاحظ أمرين ذكرناهما في [مقال الشهادات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/#certificate-types): أنها تغطي مستوى واحداً فقط، فلا تغطي `a.acme.app.example`، وأن Let's Encrypt لا تصدرها إلا بالتحقق عبر ال DNS (DNS-01 Challenge) كما في [وثائق أنواع التحقق](https://letsencrypt.org/docs/challenge-types/?ref=arabroot.io)، لذلك يحتاج Caddy مثلاً وحدة لمزود ال DNS كما شرحنا في [دليل Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/#plugins).

والتطبيق لا يثق باسم النطاق وحده، فبعد أن يحول `acme` إلى رقم المستأجر Tenant من جدول المستأجرين، يفحص أن المستخدم المسجل دخوله عضو في هذا المستأجر، وإلا فمستخدم globex يستطيع أن يكتب `acme.app.example` في المتصفح ومعه ملف تعريف الارتباط Cookie الخاص به إذا كان مضبوطاً على النطاق الرئيسي كله.

### نطاق العميل الخاص Custom Domain

العملاء الكبار يطلبون أن يكون التطبيق على نطاقهم، مثل `portal.acme.com`، فيضيف العميل سجل CNAME يشير إلى تطبيقك كما في [قسم CNAME](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#cname)، ويضيف التطبيق جدولاً يربط كل نطاق بمستأجره Tenant. والمشكلة في الشهادة: لا تستطيع أن تصدر شهادة شاملة لنطاقات لا تملكها، ولا تعرف هذه النطاقات مسبقاً. والحل الذي يقدمه Caddy اسمه [On-Demand TLS](https://caddyserver.com/docs/automatic-https?ref=arabroot.io#on-demand-tls)، وتصفه الوثائق بأنه يحصل على الشهادة أثناء أول مصافحة TLS تحتاجها، بدل أن يحصل عليها عند تحميل الإعداد، وأنه مناسب بالضبط عندما لا تتحكم في أسماء النطاقات لأنها نطاقات العملاء. ولكن انتبه للشرط الذي تكرره الوثائق: يجب أن تقيده بنقطة سؤال `ask` يسأل Caddy فيها تطبيقك هل هذا النطاق مسجل لعميل حقيقي، والسبب أن أي شخص يستطيع توجيه أي نطاق إلى سيرفرك، ودون هذا الفحص سوف يطلب Caddy شهادات لكل نطاق يصل، فيستنزف حدود جهة الإصدار ويخزن شهادات لا تعرف أصحابها.

### المسار Path والترويسة Header والحقل داخل التوكن Claim

وهناك ثلاث طرق أخرى تقابلها كثيراً:

- **المسار Path:** مثل `/t/acme/invoices`، وهو مناسب عندما لا تريد نطاقات فرعية، والقاعدة نفسها: المسار يقول ما يريده المستخدم، والفحص يقرر هل يحق له.
- **الترويسة Header:** مثل `X-Tenant-ID`، وهي مقبولة بين خدماتك الداخلية بعد أن تحدد البوابة المستأجر Tenant وتضعها، وغير مقبولة من المتصفح دون فحص العضوية.
- **حقل داخل التوكن Claim:** عندما يصدر مزود الهوية توكناً فيه حقل مثل `org_id`، يقرأ التطبيق المستأجر Tenant من التوكن الموقع بدل أن يسأل عنه، وقد شرحنا الحقول القياسية في [مقال الجلسات و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/#jwt-claims) و[مقال OAuth وOpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#claims). ولاحظ أن المستخدم الذي ينتمي إلى أكثر من مستأجر يحتاج طريقة لاختيار المستأجر النشط، وسوف نعود لهذا في [قسم الهوية](#identity).

### حمل سياق المستأجر Tenant Context عبر التطبيق والسجلات Logs والطوابير Queues

بعد أن تحدد طبقة وسيطة Middleware المستأجر Tenant في بداية الطلب، تضعه في سياق خاص بهذا الطلب Request-scoped Context، مثل [contextvars](https://docs.python.org/3/library/contextvars.html?ref=arabroot.io) في Python أو [AsyncLocalStorage](https://nodejs.org/api/async%5Fcontext.html?ref=arabroot.io) في Node.js، ثم تقرؤه كل طبقة من هناك بدل أن تمرره يدوياً من دالة إلى دالة. والهدف كما تقول [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) أن يكون كتابة كود متعدد المستأجرين قريباً من كتابة كود عادي، لأن المطور الذي يجب أن يتذكر المستأجر في كل سطر سوف ينساه في سطر ما، والحل مكتبات مشتركة تخفي التفاصيل. وفيما يلي أين يذهب السياق:

- **قاعدة البيانات:** طبقة الوصول للبيانات تضيف شرط المستأجر Tenant تلقائياً، وتنفذ `SET LOCAL app.tenant_id` في بداية كل معاملة Transaction لتعمل سياسات RLS، و`LOCAL` مهمة لأن الإعداد ينتهي مع المعاملة فلا يتسرب عبر المجمع إلى طلب آخر.
- **السجلات Logs:** كل سطر يحمل `tenant=acme`، والسبب أن أول سؤال في أي بلاغ من عميل هو «ماذا حدث لهذا العميل؟»، ودون هذا الحقل تبحث في سجلات الجميع.
- **الطوابير Queues:** الرسالة تحمل `tenant_id` داخلها، والعامل Worker الذي يقرؤها بعد دقائق لا يوجد عنده طلب HTTP يعرف منه المستأجر Tenant، لذلك يعيد ضبط السياق من الرسالة قبل أن يعمل أي شيء، وإلا نفذ المهمة دون مستأجر أو بمستأجر الرسالة السابقة.
- **الخدمات الأخرى:** عندما تستدعي خدمة داخلية ترسل معها التوكن أو الترويسة الداخلية، فلا تحتاج الخدمة أن تخمن.

### مفتاح ال Cache يجب أن يحمل المستأجر Tenant

ال Cache هو المكان الذي يتسرب منه المستأجرون Tenants أكثر من غيره، والسبب أن المطور يكتب مفتاحاً مثل `stats:unpaid` لإحصائية في لوحة التحكم، ويختبرها بمستأجر واحد في بيئة التطوير فتعمل، ثم في الإنتاج Production يحفظ أول طلب نتيجة acme تحت هذا المفتاح، ويقرؤها كل مستأجر بعده حتى تنتهي صلاحيتها. ولاحظ أن RLS في قاعدة البيانات لا يحميك هنا إطلاقاً، لأن الطلب الثاني لم يصل إلى قاعدة البيانات أصلاً، وسوف ترى هذا بعينك في [التجربة](#lab-cache). والقاعدة: كل مفتاح لبيانات تخص مستأجراً يبدأ بالمستأجر، مثل `t:acme:stats:unpaid`، وأفضل من القاعدة أن تبني دالة واحدة تصنع المفاتيح من السياق فلا يكتبها أحد يدوياً. ونفس الفكرة في ال CDN وال Reverse Proxy، وقد شرحنا في [قسم مفتاح ال Cache](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/#cache-key) و[تجربة الصفحة الخاصة التي تتسرب](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/#lab-private) كيف تحفظ نسخة واحدة لصفحة تختلف من مستخدم لآخر، وشرحنا الاتصال بـ Redis وValkey في [مقال تجميع الاتصالات](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redis-pool).

📌

في 20 مارس 2023 أوقفت OpenAI خدمة ChatGPT بعد أن رأى بعض المستخدمين عناوين محادثات مستخدمين آخرين، وشرحت في [تقريرها](https://openai.com/index/march-20-chatgpt-outage/?ref=arabroot.io) أنها تحفظ بيانات المستخدمين في Redis، وأن مكتبة redis-py تتشارك مجمع اتصالات، وأن طلباً يلغى بعد إرساله وقبل قراءة رده يترك الاتصال في حالة فاسدة، فيستلم الطلب التالي على نفس الاتصال رد طلب آخر، وأحياناً يكون الرد من النوع المتوقع فيبدو صحيحاً وهو يخص مستخدماً آخر. وذكرت أن نفس الخطأ ربما كشف معلومات دفع جزئية لـ 1.2% من مشتركي ChatGPT Plus النشطين خلال نافذة تسع ساعات، ومن الإجراءات التي أضافتها فحص إضافي يتأكد أن البيانات العائدة من ال Cache تخص المستخدم الذي طلبها. والحادثة بين مستخدمين وليس بين مستأجرين، ولكن الدرس نفسه: احفظ المستأجر داخل القيمة أيضاً، وافحصه عند القراءة، فالمفتاح الصحيح لا يحميك من خطأ في طبقة لا تراها.

## الهوية Identity والصلاحيات Authorization لكل مستأجر Tenant

في تطبيق لمستأجر Tenant واحد يكفي جدول مستخدمين وصفحة دخول، أما في تطبيق متعدد المستأجرين فتظهر أسئلة جديدة: كيف يعرف التطبيق مستأجر المستخدم قبل أن يدخل؟ وماذا عن مستشار يعمل مع شركتين؟ وماذا لو أرادت acme أن يدخل موظفوها بحساباتهم في مزود الهوية الخاص بها؟ والصورة التالية تبين الصورة الكاملة:

![على اليمين acme ومزود هويتها الخاص يرسل الدخول الموحد SSO عبر SAML أو OIDC في الخطوة 1 والحسابات عبر SCIM في الخطوة 2 إلى طبقة الهوية في الوسط، وعلى اليسار globex دون مزود هوية تدخل بكلمة المرور والتحقق الثنائي، وفي الوسط طبقة هوية فيها منظمة acme مرتبطة بنطاق acme.com ومنظمة globex بدخول محلي وتوكن فيه sub وorg_id وroles، وتحتها في الخطوة 3 جدول العضويات حيث سارة admin في acme وviewer في globex، وفي الخطوة 4 فحص الصلاحية الذي يشترط أن تكون الفاتورة في المستأجر النشط وأن يسمح دور المستخدم فيه](https://arabroot.io/content/images/2026/10/multi-tenancy-05-identity.webp)

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

- **دخول يعرف المستأجر Tenant-aware Login:** التطبيق يعرف المستأجر من النطاق الفرعي، أو يسأل عن البريد أولاً ثم يحدد من نطاق البريد أين يرسل المستخدم. وفي [Keycloak](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io) توجد لهذا ميزة المنظمات Organizations داخل ال Realm الواحد، فلكل منظمة أعضاؤها ونطاقات بريدها ومزودو الهوية المرتبطون بها، ويوجه المستخدم إلى مزود منظمته حسب نطاق بريده، وأعلن المشروع أن [المنظمات مدعومة بالكامل منذ الإصدار 26](https://www.keycloak.org/2024/10/keycloak-2600-released?ref=arabroot.io). والبديل الأقدم Realm لكل مستأجر، وال Realms معزولة عن بعضها كما تقول الوثائق، ولكن عددها يكبر مع عدد العملاء ويصبح كل Realm شيئاً تديره.
- **مستخدم في أكثر من مستأجر Tenant:** مستشار أو محاسب خارجي يعمل مع acme وglobex بنفس البريد، فلا تجعل المستخدم ينتمي إلى مستأجر واحد في جدول المستخدمين، وإنما اجعل جدول عضويات Memberships فيه المستخدم والمستأجر والدور، وأعطه طريقة لاختيار المستأجر النشط، ويصدر التوكن أو الجلسة لهذا المستأجر وحده.
- **الأدوار لكل مستأجر Roles per Tenant:** سارة مديرة admin في acme ومشاهدة viewer في globex، لذلك لا يوجد دور «مدير» عام، ويوجد دور في مستأجر.
- **الدخول الموحد SSO لكل عميل:** العميل الكبير يأتي بمزود هويته عبر SAML أو OIDC، وقد شرحنا الفرق في [قسم المقارنة في مقال OAuth](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#comparison)، وفي تطبيقك يصبح لكل مستأجر Tenant إعداد مزود هوية خاص به، وغالباً تجعله ميزة في الباقات الأعلى.
- **تعيين الحسابات SCIM لكل مستأجر Tenant:** عندما يغادر موظف acme يجب أن يتعطل حسابه عندك تلقائياً، وهذا ما يفعله [SCIM](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-scim-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%B3%D8%A7%D8%A8%D8%A7%D8%AA-%D8%AA%D9%84%D9%82%D8%A7%D8%A6%D9%8A%D8%A7/#what-is-scim)، ولكل مستأجر توكن SCIM خاص به لا يستطيع إنشاء مستخدم إلا داخل مستأجره.
- **المستأجر Tenant في كل فحص صلاحية:** السؤال ليس «هل سارة admin؟» وإنما «هل سارة admin في المستأجر الذي تتبع له هذه الفاتورة، وهل هذا هو المستأجر النشط في الطلب؟»، وقد شرحنا في [مقال الصلاحيات المركزية](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/#rebac) كيف تمثل العلاقات بين المستخدم والمنظمة والكائن في محرك مثل OpenFGA، والمنظمة هنا هي المستأجر.

أما authentik فلاحظ الفرق بين مفهومين فيه حتى لا يختلط عليك: [العلامات Brands](https://docs.goauthentik.io/brands/?ref=arabroot.io) تغير مظهر صفحة الدخول وسلوكها حسب النطاق، و[التعدد Tenancy](https://docs.goauthentik.io/sys-mgmt/tenancy/?ref=arabroot.io) ميزة مختلفة متاحة منذ الإصدار 2024.2 تنشئ مستأجرين مستقلين لكل واحد منهم مخطط PostgreSQL خاص، وتذكر الوثائق أنها ما زالت ألفا Alpha وتحتاج ترخيص Enterprise، وأن ال Brands كانت تسمى سابقاً Tenants.

## الجار المزعج Noisy Neighbor والعدل بين المستأجرين Tenants

تعرف [AWS SaaS Lens](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/noisy-neighbor.html?ref=arabroot.io) الجار المزعج Noisy Neighbor بأنه مستخدم يضع حملاً على موارد النظام يؤثر سلباً على المستخدمين الآخرين، وتذكر أن المشكلة أوضح في الأنظمة متعددة المستأجرين لأن حمل كل مستأجر Tenant لا يمكن توقعه. ومثال ذلك عميل كبير يصدر تقرير السنة كاملة في التاسعة صباحاً فيأخذ كل العمال وكل اتصالات قاعدة البيانات، فتبطؤ الصفحات عند مئة عميل صغير لا علاقة لهم بالتقرير. والحل الأول الذي يخطر على البال أن تكبر السيرفر، وهذا يؤجل المشكلة فقط، لأن العميل الكبير سوف يأخذ السيرفر الأكبر أيضاً، والحل الأنسب أن تضع حدوداً لكل مستأجر كما في الصورة التالية:

![على اليمين دون حدود: المستأجر initech يرسل 5000 طلب في الدقيقة وتصديراً كبيراً فيصبح كل العمال مشغولين واتصالات قاعدة البيانات 100% والمستأجران الصغيران acme وglobex ينتظران، وعلى اليسار حد للطلبات لكل مستأجر يرد على initech بالرمز 429 بعد حصته، ثم التطبيق وثلاثة طوابير منفصلة لكل مستأجر يأخذ منها العمال بالدور، وقاعدة البيانات على 60%، وفي الأسفل مقاييس لكل مستأجر مثل http_requests_total بحقل tenant وفاتورة الاستهلاك](https://arabroot.io/content/images/2026/10/multi-tenancy-06-noisy-neighbor.webp)

دون حدود يأخذ الأعلى صوتاً كل الموارد، ومع حدود لكل مستأجر Tenant يحصل كل واحد على نصيبه

- **حد الطلبات لكل مستأجر Rate Limit per Tenant:** المفتاح في حد الطلبات هو المستأجر وليس عنوان IP وحده، وفي Nginx مثلاً تقول [وثائق limit\_req\_zone](https://nginx.org/en/docs/http/ngx%5Fhttp%5Flimit%5Freq%5Fmodule.html?ref=arabroot.io) إن المفتاح يمكن أن يكون أي متغير، وإن الطلبات التي قيمة مفتاحها فارغة لا تحسب، وهذا يعني أن الطلب الذي لم يحدد مستأجره لا يخضع للحد إطلاقاً، فتأكد أن البوابة ترفضه قبل ذلك. وقد شرحنا إعداد حد الطلبات في كل أداة في [مقال حماية تطبيقات الويب](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%B9%D9%84%D9%89-%D9%85%D8%B3%D8%AA%D9%88%D9%89-http/#rate-limiting)، والرد الصحيح عند تجاوز الحد هو 429 Too Many Requests.
- **الحصص Quotas:** عدد المستخدمين، ومساحة التخزين، وعدد التقارير الثقيلة في اليوم، وهي حدود يفحصها التطبيق نفسه وتأتي من باقة المستأجر Tenant.
- **الطوابير Queues لكل مستأجر Tenant أو لكل أولوية:** العمل الثقيل لا يحدث داخل الطلب، وإنما يذهب إلى طابور كما شرحنا في [المدخل إلى تصميم الأنظمة](https://arabroot.io/articles/%D9%85%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D9%85%D9%86-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86/#queues)، ولكن طابوراً واحداً للجميع يعني أن عشرة آلاف مهمة من مستأجر واحد تقف أمام مهمة واحدة من مستأجر آخر، والحل أن يأخذ العمال من طوابير المستأجرين بالدور، أو أن يكون طابور لكل باقة مع عدد عمال لكل طابور.
- **حدود الموارد Resource Limits:** في قاعدة البيانات مهلة للاستعلام `statement_timeout` تمنع استعلاماً واحداً من أن يعمل ساعة، وفي Kubernetes حصص Resource Quotas لكل Namespace عندما يكون للمستأجر Tenant حوسبة خاصة.
- **المراقبة لكل مستأجر Tenant:** تطلب [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) لوحات تشغيل تعرض حمل كل مستأجر وكل باقة، لأن السؤال الأول عند البطء «من الذي يسببه؟». ولكن احذر في Prometheus، فوثائق [التسمية فيه](https://prometheus.io/docs/practices/naming/?ref=arabroot.io) تحذر من أن كل تركيبة قيم للحقول Labels سلسلة زمنية Time Series جديدة، وتمنع استخدام حقول بقيم كثيرة لا حد لها مثل أرقام المستخدمين، فإذا كان عندك عشرون مستأجراً فحقل `tenant` مقبول، وإذا كانوا عشرين ألفاً فضع الباقة في المقاييس، والمستأجر في السجلات والتتبع Tracing حيث يمكن البحث عنه.
- **القياس والفوترة Metering:** نفس الأرقام تجيب عن سؤال تطرحه [AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) صراحة: هل عملاء الباقة الأساسية يكلفون أكثر من عملاء الباقة العليا؟ ودون قياس لكل مستأجر Tenant قد تكتشف أن أرخص باقة هي أغلى عميل. وإذا كانت الفوترة بالاستهلاك Usage-based Billing فهناك أدوات مفتوحة المصدر لجمع الاستهلاك مثل [OpenMeter](https://github.com/openmeterio/openmeter?ref=arabroot.io) (Apache-2.0) و[Lago](https://github.com/getlago/lago?ref=arabroot.io) (AGPL-3.0).

## إعدادات كل مستأجر Tenant Configuration دون نسخة خاصة من الكود

يأتي العميل الكبير ويقول: نريد حقلاً إضافياً في الفاتورة، ونريد تعطيل ميزة التصدير لموظفينا، ونريد أن يكون الحد أعلى. والحل البسيط نسخة من المستودع Fork لهذا العميل، أو شرط `if tenant == 'acme'` في الكود، وهذا يعمل مع عميل واحد، ثم مع العميل العاشر يصبح عندك عشر نسخ لا تعرف أي إصلاح وصل إلى أيها، وهذا عكس السبب الذي من أجله اخترت تعدد المستأجرين Tenants. والقاعدة التي تكتبها [AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io) أن تحول الطلب الخاص إلى خيار إعداد متاح لكل العملاء Global Customization، و[إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) تعد النسخ الخاصة وإضافة أعمدة لعميل واحد من الأنماط التي يجب تجنبها. وفيما يلي الطريقة الصحيحة:

- **جدول إعدادات المستأجر Tenant:** الباقة، والحدود، والمنطقة، ومزود الهوية، والشعار والألوان، كلها صفوف في قاعدة البيانات يقرؤها الكود، وليست شروطاً فيه.
- **أعلام الميزات Feature Flags:** الميزة موجودة في الكود للجميع، والعلم يقرر لمن تظهر حسب المستأجر Tenant أو الباقة، ومعيار [OpenFeature](https://openfeature.dev/?ref=arabroot.io) يوحد طريقة سؤال الكود عن العلم، ومن الأدوات مفتوحة المصدر [Unleash](https://github.com/Unleash/unleash?ref=arabroot.io) (AGPL-3.0) و[Flagsmith](https://github.com/Flagsmith/flagsmith?ref=arabroot.io) (BSD-3-Clause).
- **الحقول المخصصة Custom Fields:** بدل عمود جديد لعميل واحد، عمود JSONB للخصائص المرنة أو جدول للحقول المخصصة، وقد شرحنا متى يناسب JSONB في [مقال تصميم قواعد البيانات](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/#jsonb).

## دورة حياة المستأجر Tenant Lifecycle من التسجيل إلى الحذف

هنا نعرض الصورة العامة فقط، والتنفيذ خطوة بخطوة في [المقال الثاني](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/):

1. **التسجيل Onboarding:** إنشاء المستأجر Tenant يجب أن يكون عملية واحدة مؤتمتة تتكرر بنفس الشكل كما تطلب [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io): صف في جدول المستأجرين، والمدير الأول، والمنظمة في مزود الهوية، والإعدادات حسب الباقة، وفي النموذج المعزول قاعدة البيانات أو المخطط أيضاً، والنطاق الفرعي يعمل تلقائياً بفضل السجل العام.
2. **التحديث Upgrades:** إصدار واحد للجميع، وترحيل الجداول يمر على كل قاعدة أو مخطط إذا لم تكن الجداول مشتركة، و[إرشادات Azure](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/storage-data?ref=arabroot.io) تنصح بأن يتوافق التطبيق مع النسخة السابقة من الجداول على الأقل، لأن المستأجرين Tenants لن ينتقلوا كلهم في نفس اللحظة، وقد شرحنا تغيير الجداول دون توقف في [مقال تصميم قواعد البيانات](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/#migrations).
3. **النقل:** عميل كبر فتنقله من القاعدة المشتركة إلى قاعدة خاصة، أو من منطقة إلى أخرى، وهذا ممكن دون تغيير الكود فقط إذا كان كل صف يحمل مستأجره Tenant من البداية.
4. **المغادرة Offboarding:** تصدير بيانات العميل له بصيغة يستطيع قراءتها، ثم حذفها. وفي اللائحة الأوروبية العامة لحماية البيانات [GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/oj?ref=arabroot.io) تنص المادة 28 على أن العقد بين المتحكم Controller والمعالج Processor يلزم المعالج بأن يحذف البيانات الشخصية أو يعيدها إلى المتحكم حسب اختياره بعد انتهاء الخدمة، وفي المملكة يعطي [نظام حماية البيانات الشخصية](https://laws.boe.gov.sa/BoeLaws/Laws/LawDetails/b7cfae89-828e-4994-b167-adaa00e37188/1?ref=arabroot.io) في مادته الرابعة صاحب البيانات الحق في الحصول على نسخة منها بصيغة مقروءة وواضحة، والحق في طلب إتلافها عندما تنتهي الحاجة إليها. وهذا ليس استشارة قانونية، فراجع المستشار القانوني لما ينطبق على منتجك، والذي يهمك تقنياً أن يكون التصدير والحذف لكل مستأجر Tenant أداة جاهزة، وأن تعرف أن النسخ الاحتياطية تحتفظ بالبيانات حتى تنتهي مدة الاحتفاظ بها.

## كيف فعلتها شركات معروفة؟ أمثلة من Notion وSlack وFigma

فيما يلي ثلاث تجارب منشورة من مدونات الشركات الهندسية نفسها، اخترناها لأن كل واحدة تعلمك شيئاً مختلفاً:

- **Notion: التوزيع حسب مساحة العمل:** في [مقالها في أكتوبر 2021](https://www.notion.com/blog/sharding-postgres-at-notion?ref=arabroot.io) شرحت Notion أنها وزعت بيانات PostgreSQL حسب رقم مساحة العمل Workspace ID، لأن كل كتلة Block تنتمي إلى مساحة عمل واحدة بالضبط، والمستخدم يستعلم غالباً داخل مساحة عمل واحدة، فلا تحتاج أغلب الاستعلامات إلى أكثر من جزء، وكان التوزيع 480 جزءاً منطقياً Logical Shards على 32 قاعدة بيانات فعلية. ومساحة العمل هنا هي المستأجر Tenant.
- **Slack: عندما يكبر المستأجر Tenant أكثر من السيرفر:** في [مقالها في ديسمبر 2020](https://slack.engineering/scaling-datastores-at-slack-with-vitess/?ref=arabroot.io) شرحت Slack أن كل جزء Shard في قواعدها كان يحمل كل بيانات مساحة عمل كاملة، وأن الجزء الواحد يحمل آلاف المساحات، ثم مع العملاء الكبار وصل الجزء المخصص لبعضهم إلى أكبر عتاد متاح، ولم تستطع توزيع حملهم فظهرت نقاط ساخنة Hot Spots، كما أن ميزة Slack Connect التي تجمع أكثر من شركة في قناة واحدة لا تناسب افتراض أن بيانات الفريق كلها في جزء واحد، فانتقلت إلى Vitess. والدرس أن التوزيع حسب المستأجر ممتاز حتى يصبح مستأجر واحد أكبر من سيرفر، فخطط لهذا اليوم.
- **Figma: المستأجر Tenant ليس دائماً مفتاح التوزيع:** في [مقالها في مارس 2024](https://www.figma.com/blog/how-figmas-databases-team-lived-to-tell-the-scale/?ref=arabroot.io) ذكرت Figma أنها لم تجد مفتاحاً واحداً مناسباً في نموذج بياناتها، فاختارت عدة مفاتيح هي UserID وFileID وOrgID، وجمعت الجداول التي تتشارك نفس المفتاح في مجموعات سمتها Colos تدعم الربط والمعاملات داخل المفتاح الواحد. والدرس أن رقم المستأجر هو البداية الطبيعية، ولكن بياناتك هي التي تقرر.

وقد رأينا في [مقال Stack Overflow](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A8%D9%86%D9%89-stack-overflow-%D9%85%D9%88%D9%82%D8%B9%D9%87-%D8%A8%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA-%D9%82%D9%84%D9%8A%D9%84%D8%A9/#request-path) تطبيقاً واحداً متعدد المستأجرين Tenants يخدم الشبكة كلها، ورأينا في [قسم انتقال Teams إلى Azure](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A8%D9%86%D9%89-stack-overflow-%D9%85%D9%88%D9%82%D8%B9%D9%87-%D8%A8%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA-%D9%82%D9%84%D9%8A%D9%84%D8%A9/#teams-azure) كيف استغرق فصل منتج Teams عن بنية المواقع العامة ونقله حوالي سنتين، وهذا وحده سبب كاف لأن تفكر في حدود المستأجرين مبكراً.

## مشاريع مفتوحة المصدر Open Source تطبق هذه الأنماط

| المشروع                                                                                     | النمط                                                               | الرخصة                           | ملاحظات                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Citus](https://github.com/citusdata/citus?ref=arabroot.io)                                 | توزيع PostgreSQL على عدة سيرفرات حسب tenant\_id أو حسب المخطط       | AGPL-3.0                         | يوجه الاستعلام الذي فيه شرط المستأجر Tenant إلى سيرفر واحد، وقد شرحنا متى تحتاجه في [مقال PostgreSQL للجداول الضخمة](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/#citus) |
| [django-tenants](https://github.com/django-tenants/django-tenants?ref=arabroot.io)          | مخطط لكل مستأجر Tenant في Django                                    | MIT                              | يحدد المستأجر Tenant من اسم النطاق ويغير search\_path                                                                                                                                                                                                |
| [apartment](https://github.com/rails-on-services/apartment?ref=arabroot.io) (ros-apartment) | مخطط أو قاعدة لكل مستأجر Tenant في Rails                            | MIT                              | المستودع الأصلي [influitive/apartment](https://github.com/influitive/apartment?ref=arabroot.io) لم يحدث منذ 2024، والتطوير مستمر في هذا الفرع                                                                                                        |
| [acts\_as\_tenant](https://github.com/ErwinM/acts%5Fas%5Ftenant?ref=arabroot.io)            | جداول مشتركة مع عمود المستأجر Tenant في Rails                       | MIT                              | يضيف شرط المستأجر Tenant تلقائياً على الاستعلامات                                                                                                                                                                                                    |
| [Keycloak](https://github.com/keycloak/keycloak?ref=arabroot.io)                            | الهوية: المنظمات Organizations أو Realm لكل مستأجر Tenant           | Apache-2.0                       | المنظمات مدعومة بالكامل منذ الإصدار 26                                                                                                                                                                                                               |
| [authentik](https://github.com/goauthentik/authentik?ref=arabroot.io)                       | الهوية: Brands حسب النطاق، وTenancy بمخطط لكل مستأجر Tenant         | MIT، ومجلد Enterprise برخصة خاصة | ميزة Tenancy ألفا وتحتاج ترخيص Enterprise                                                                                                                                                                                                            |
| [Capsule](https://github.com/projectcapsule/capsule?ref=arabroot.io)                        | الحوسبة: تجميع Namespaces لكل مستأجر Tenant في Kubernetes مع سياسات | Apache-2.0                       | عزل على مستوى Namespace                                                                                                                                                                                                                              |
| [vCluster](https://github.com/loft-sh/vcluster?ref=arabroot.io)                             | الحوسبة: Cluster افتراضي لكل مستأجر Tenant                          | Apache-2.0                       | لكل مستأجر Tenant API Server خاص داخل Cluster مشترك                                                                                                                                                                                                  |

والرخص كما في صفحة كل مستودع وقت كتابة المقال، وقد شرحنا ما تعنيه AGPL إذا عدلت المشروع وقدمته خدمة للآخرين في [مقال البرمجيات مفتوحة المصدر وتراخيصها](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/).

## أي نموذج عزل Isolation Model تختار؟ دليل قرار

لا يوجد نموذج صحيح لكل الحالات، وهذا أول مبدأ في [مبادئ AWS](https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/general-design-principles.html?ref=arabroot.io)، ولكن يوجد ترتيب معقول للأسئلة، والصورة التالية تلخصه:

![مخطط قرار يبدأ بمستأجر جديد أو منتج جديد، ثم سؤال هل يفرض نظام أو عقد فصلاً فعلياً أو مكاناً محدداً أو مفاتيح تشفير خاصة، ونعم تذهب إلى Silo بقاعدة بيانات خاصة تحت نفس طبقة التحكم، ولا تذهب إلى سؤال هل تتوقع مئات المستأجرين وأغلبهم صغار، ولا تذهب إلى Schema أو DB لكل مستأجر مع أتمتة الترحيل، ونعم تذهب إلى Pool بجداول مشتركة وtenant_id وRLS كنقطة بداية، ثم سؤال هل كبر مستأجر حتى أزعج الباقين، ونعم تذهب إلى Bridge أو Sharding مثل Citus، ولا تعني البقاء على Pool مع المراقبة والحدود](https://arabroot.io/content/images/2026/10/multi-tenancy-08-decision.webp)

ترتيب الأسئلة: الأنظمة والعقود أولاً، ثم عدد المستأجرين Tenants، ثم حجم أكبرهم

- **ابدأ بالنموذج المشترك Pool في أغلب الحالات:** جداول مشتركة، وعمود `tenant_id` في كل جدول ومفتاح، وسياسات RLS حارساً، ومفاتيح Cache تبدأ بالمستأجر Tenant، والسبب أنه الأرخص والأسهل في التحديث، والأهم أنه يجبرك على حمل السياق من اليوم الأول، وبعدها يصبح نقل مستأجر إلى قاعدة خاصة مسألة مكان وليس إعادة كتابة.
- **أعط Silo لمن يحتاجه ويدفع ثمنه:** العميل الحكومي أو الصحي الذي يفرض عليه نظام أو عقد مكان البيانات أو فصلها أو مفاتيح تشفير خاصة، في الباقة الأعلى، وتحت نفس الكود وطبقة التحكم.
- **اختر مخططاً أو قاعدة لكل مستأجر Tenant من البداية فقط إذا كان عملاؤك قليلين وكباراً:** مثل نظام لعشرين مستشفى، لكل واحد نسخته الاحتياطية وتعديلاته، بشرط أن تؤتمت الإنشاء والترحيل من اليوم الأول.
- **عندما يكبر مستأجر Tenant حتى يزعج الباقين:** انقله إلى قاعدة خاصة Bridge، أو وزع المستأجرين على عدة سيرفرات حسب `tenant_id`، وتذكر درس Slack: أكبر مستأجر عندك سوف يصل يوماً إلى حدود السيرفر.

وبكل تأكيد أنت لا تحتاج كل هذا في الأسبوع الأول، والذي لا يمكن تأجيله هو عمود `tenant_id` في كل جدول والسياق في كل طلب، لأن إضافتهما لاحقاً تعني المرور على كل جدول وكل استعلام في المشروع.

## تجربة صغيرة: كيف يتسرب مستأجر Tenant إلى آخر؟

التجربة التالية صغيرة ومقصودة لترى بعينك التسرب في مكانين: استعلام نسي شرط المستأجر Tenant، ومفتاح Cache لا يحمل المستأجر، ثم كيف يمنع الحارس الأول ولا يمنع الثاني. ونحتاج PostgreSQL 18 وValkey 9 في Docker، والمنافذ على `127.0.0.1` فقط.

### تجهيز قاعدة البيانات Database وال Cache

```bash
docker network create lab-mt-net
docker run -d --name lab-mt-pg --network lab-mt-net -p 127.0.0.1:19580:5432 \
  -e POSTGRES_PASSWORD=labpass -e POSTGRES_DB=saas postgres:18
docker run -d --name lab-mt-valkey --network lab-mt-net -p 127.0.0.1:19581:6379 valkey/valkey:9-alpine
docker exec lab-mt-pg psql -U postgres -tAc 'select version()'
docker exec lab-mt-valkey valkey-server --version
```

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

```text
PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
Valkey server v=9.1.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=4784f0215171d89
```

الآن نقوم بإنشاء جدول للمستأجرين Tenants وجدول فواتير مشترك فيه مستأجران، acme شركة تجارية وglobex عيادات:

```bash
docker exec -i lab-mt-pg psql -U postgres -d saas <<'SQL'
CREATE TABLE tenants (
  id   text PRIMARY KEY,
  name text NOT NULL
);
CREATE TABLE invoices (
  tenant_id text    NOT NULL REFERENCES tenants(id),
  id        bigint  GENERATED ALWAYS AS IDENTITY,
  customer  text    NOT NULL,
  amount    numeric NOT NULL,
  paid      boolean NOT NULL DEFAULT false,
  PRIMARY KEY (tenant_id, id)
);
INSERT INTO tenants VALUES ('acme', 'Acme Trading'), ('globex', 'Globex Clinics');
INSERT INTO invoices (tenant_id, customer, amount, paid) VALUES
  ('acme',   'Riyadh Office Supplies', 1200, false),
  ('acme',   'Jeddah Logistics',        800, false),
  ('acme',   'Dammam Print',            450, true),
  ('globex', 'Patient 1043',            300, false),
  ('globex', 'Patient 2210',            950, true);
SQL
```

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

```text
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 5
```

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

- المفتاح الأساسي `PRIMARY KEY (tenant_id, id)` يبدأ بالمستأجر Tenant كما ذكرنا في [قسم الجداول المشتركة](#shared-tables).
- الرقم `id` يولد من تسلسل واحد للجدول كله، فأرقام فواتير globex تكمل بعد أرقام acme، وهذا تفصيل صغير سوف يكشف لنا ثغرة بعد قليل.

### الخطوة 1: الاستعلام الذي نسي المستأجر Tenant

لنفرض أن موظفاً من globex فتح صفحة «الفواتير غير المدفوعة»، والمطور كتب الاستعلام ونسي الشرط، ثم نقارنه بالاستعلام الصحيح:

```bash
echo "== forgotten filter"
docker exec lab-mt-pg psql -U postgres -d saas -c "SELECT tenant_id, id, customer, amount FROM invoices WHERE paid = false ORDER BY id;"
echo "== with filter"
docker exec lab-mt-pg psql -U postgres -d saas -c "SELECT tenant_id, id, customer, amount FROM invoices WHERE tenant_id = 'globex' AND paid = false ORDER BY id;"
```

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

```text
== forgotten filter
 tenant_id | id |        customer        | amount
-----------+----+------------------------+--------
 acme      |  1 | Riyadh Office Supplies |   1200
 acme      |  2 | Jeddah Logistics       |    800
 globex    |  4 | Patient 1043           |    300
(3 rows)

== with filter
 tenant_id | id |   customer   | amount
-----------+----+--------------+--------
 globex    |  4 | Patient 1043 |    300
(1 row)
```

موظف العيادة رأى عملاء الشركة التجارية ومبالغهم. والآن الثغرة الثانية: صفحة تفاصيل الفاتورة `/invoices/1` تبحث بالرقم وحده، وموظف globex يرى أن فاتورته رقم 4، فيجرب الرقم 1:

```bash
docker exec lab-mt-pg psql -U postgres -d saas -c "SELECT tenant_id, id, customer, amount FROM invoices WHERE id = 1;"
```

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

```text
 tenant_id | id |        customer        | amount
-----------+----+------------------------+--------
 acme      |  1 | Riyadh Office Supplies |   1200
(1 row)
```

وهذه هي ثغرة IDOR التي ذكرناها، والأرقام المتسلسلة تجعل تخمينها أسهل، ولكن الحل ليس رقماً يصعب تخمينه، وإنما شرط المستأجر Tenant في كل استعلام، والأفضل منه حارس لا ينسى.

### الخطوة 2: الحارس في قاعدة البيانات Row-Level Security

نقوم الآن بإنشاء دور للتطبيق ليس مالكاً للجدول، ونفعل RLS بسياسة واحدة تقرأ المستأجر Tenant من الإعداد `app.tenant_id`. وهذا مثال للفكرة فقط، أما الإعداد الكامل مع `FORCE` والأدوار والمجمع والاختبارات فهو في [المقال الثاني](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/):

```bash
docker exec -i lab-mt-pg psql -U postgres -d saas <<'SQL'
CREATE ROLE app_user LOGIN PASSWORD 'apppass';
GRANT SELECT, INSERT, UPDATE, DELETE ON invoices TO app_user;
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.tenant_id', true));
SQL
```

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

```text
CREATE ROLE
GRANT
ALTER TABLE
CREATE POLICY
```

الآن نتصل بدور التطبيق وننفذ نفس الاستعلامين الخاطئين، مرة دون مستأجر Tenant، ومرة بعد أن نحدد أن الطلب يخص globex:

```bash
docker exec -i -e PGPASSWORD=apppass lab-mt-pg psql -h 127.0.0.1 -U app_user -d saas <<'SQL'
-- 1) no tenant set yet: the forgotten filter again
SELECT tenant_id, id, customer FROM invoices WHERE paid = false;
-- 2) the request belongs to globex
BEGIN;
SET LOCAL app.tenant_id = 'globex';
SELECT tenant_id, id, customer FROM invoices WHERE paid = false;
SELECT tenant_id, id, customer FROM invoices WHERE id = 1;
COMMIT;
SQL
echo "== as owner"
docker exec lab-mt-pg psql -U postgres -d saas -c "SELECT count(*) FROM invoices;"
```

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

```text
 tenant_id | id | customer
-----------+----+----------
(0 rows)

BEGIN
SET
 tenant_id | id |   customer
-----------+----+--------------
 globex    |  4 | Patient 1043
(1 row)

 tenant_id | id | customer
-----------+----+----------
(0 rows)

COMMIT
== as owner
 count
-------
     5
(1 row)
```

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

- دون مستأجر Tenant محدد لم يظهر أي صف، لأن `current_setting('app.tenant_id', true)` يعيد NULL عندما لا يوجد الإعداد بدل أن يرمي خطأ، والمقارنة مع NULL لا تكون صحيحة أبداً، فالخطأ ينتهي بصفحة فارغة وليس بتسرب.
- بعد `SET LOCAL app.tenant_id = 'globex'` أعاد نفس الاستعلام الذي نسي الشرط فاتورة globex وحدها، والبحث بالرقم 1 أعاد صفراً من الصفوف بدل فاتورة acme.
- المستخدم `postgres` رأى الصفوف الخمسة، لأنه مستخدم خارق ومالك الجدول، وهذا ما تقوله الوثائق بالضبط، لذلك لا يتصل التطبيق بهذا المستخدم أبداً.

### الخطوة 3: مفتاح Cache دون مستأجر Tenant

الآن نبني «التطبيق» في دالة Shell صغيرة تمثل مربع «الفواتير غير المدفوعة» في لوحة التحكم، وتحفظ النتيجة في Valkey لمدة ستين ثانية. احفظ الملف التالي باسم `report.sh`:

```bash
# report.sh: the "unpaid invoices" widget on the dashboard, cached for 60 seconds
report() {
  tenant="$1"                      # the tenant this request belongs to
  key="stats:unpaid"               # the cache key
  cached=$(docker exec lab-mt-valkey valkey-cli GET "$key")
  if [ -n "$cached" ]; then
    echo "request from $tenant -> HIT  key=$key value=$cached"
    return
  fi
  value=$(echo "SELECT json_build_object('tenant', current_setting('app.tenant_id'), 'unpaid', count(*), 'total', coalesce(sum(amount), 0)) FROM invoices WHERE paid = false;" |
    docker exec -i -e PGPASSWORD=apppass -e PGOPTIONS="-c app.tenant_id=$tenant" \
      lab-mt-pg psql -h 127.0.0.1 -U app_user -d saas -tA)
  docker exec lab-mt-valkey valkey-cli SET "$key" "$value" EX 60 > /dev/null
  echo "request from $tenant -> MISS key=$key value=$value"
}
```

لاحظ أن الدالة تتصل بدور التطبيق `app_user` وتحدد المستأجر Tenant في الاتصال، أي أن RLS يعمل، ثم نرسل طلباً من acme وطلبين من globex:

```bash
docker exec lab-mt-valkey valkey-cli FLUSHALL
. ./report.sh
report acme
report globex
report globex
```

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

```text
OK
request from acme -> MISS key=stats:unpaid value={"tenant" : "acme", "unpaid" : 2, "total" : 2000}
request from globex -> HIT  key=stats:unpaid value={"tenant" : "acme", "unpaid" : 2, "total" : 2000}
request from globex -> HIT  key=stats:unpaid value={"tenant" : "acme", "unpaid" : 2, "total" : 2000}
```

موظف globex رأى أن عنده فاتورتين غير مدفوعتين بمجموع 2000، وهي أرقام acme، رغم أن سياسة RLS تعمل كما رأيت في الخطوة السابقة، والسبب أن طلبات globex لم تصل إلى قاعدة البيانات أصلاً، فقد وجدت القيمة جاهزة في ال Cache. والإصلاح أن يبدأ المفتاح بالمستأجر Tenant، فغير سطر المفتاح في `report.sh` إلى السطر التالي:

```bash
  key="t:$tenant:stats:unpaid"     # the cache key now starts with the tenant
```

ثم أعد التجربة واعرض المفاتيح الموجودة:

```bash
docker exec lab-mt-valkey valkey-cli FLUSHALL
. ./report.sh
report acme
report globex
report globex
docker exec lab-mt-valkey valkey-cli --scan --pattern '*'
```

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

```text
OK
request from acme -> MISS key=t:acme:stats:unpaid value={"tenant" : "acme", "unpaid" : 2, "total" : 2000}
request from globex -> MISS key=t:globex:stats:unpaid value={"tenant" : "globex", "unpaid" : 1, "total" : 300}
request from globex -> HIT  key=t:globex:stats:unpaid value={"tenant" : "globex", "unpaid" : 1, "total" : 300}
t:acme:stats:unpaid
t:globex:stats:unpaid
```

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

- أصبح لكل مستأجر Tenant مفتاحه، وأول طلب من globex حسب أرقامه هو، والطلب الثاني قرأ قيمته هو من ال Cache.
- القيمة نفسها تحمل `"tenant"`، وهذا يسمح بالفحص الإضافي الذي أضافته OpenAI بعد حادثتها: قبل أن تعيد القيمة تأكد أن المستأجر Tenant فيها هو مستأجر الطلب.
- الأمر `FLUSHALL` يمسح كل ما في Valkey، وهو مناسب هنا لأن ال Container خاص بالتجربة، ولا تنفذه على Valkey يستخدمه تطبيق آخر.

### حذف التجربة

نحذف ال Containers مع ال Volumes التي أنشأتها ال Images لها تلقائياً، ثم الشبكة، ثم الملف:

```bash
docker rm -f -v lab-mt-pg lab-mt-valkey
docker network rm lab-mt-net
rm -f report.sh
```

ولاحظ الخيار `-v`، فدونه يبقى ال Volume المجهول Anonymous Volume الذي تنشئه Image الخاصة بـ PostgreSQL للبيانات بعد حذف ال Container.

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

- **الثقة بالمستأجر Tenant القادم من الكلاينت:** حقل `tenant_id` في جسم الطلب أو ترويسة من المتصفح دون فحص العضوية، فيغير المستخدم القيمة ويصبح في مستأجر آخر.
- **استعلام جديد دون شرط المستأجر Tenant:** تقرير أو بحث أو تصدير كتب على عجل، والحل طبقة وصول للبيانات تضيف الشرط تلقائياً، وRLS حارساً، واختبار آلي يتأكد أن مستخدم مستأجر لا يرى بيانات غيره.
- **البحث بالمعرف وحده:** `WHERE id = $1` في صفحة التفاصيل والتعديل والحذف، والحل `WHERE tenant_id = $1 AND id = $2` دائماً.
- **مفتاح Cache دون مستأجر Tenant:** كما رأيت في التجربة، وRLS لا ينقذك هنا.
- **التطبيق يتصل بمالك الجداول أو بمستخدم خارق:** فلا تعمل سياسات RLS عليه أصلاً.
- **`SET` بدل `SET LOCAL` خلف مجمع اتصالات:** فيبقى المستأجر Tenant أو `search_path` على الاتصال ويصل إلى طلب مستأجر آخر.
- **العامل Worker دون سياق:** رسالة في الطابور لا تحمل المستأجر Tenant، فينفذ العامل المهمة على مستأجر خاطئ أو على الجميع.
- **قيود فريدة على الجدول كله:** `UNIQUE (email)` بدل `UNIQUE (tenant_id, email)`، فتمنع عميلاً وتكشف له بيانات عميل آخر.
- **نسخة خاصة لكل عميل كبير:** Fork أو شروط باسم العميل في الكود، بدل الإعدادات وأعلام الميزات.
- **حقل `tenant` في كل مقياس مع آلاف المستأجرين Tenants:** فينفجر عدد السلاسل الزمنية في Prometheus.
- **On-Demand TLS دون نقطة `ask`:** فيطلب ال Reverse Proxy شهادات لأي نطاق يوجهه أي شخص إلى سيرفرك.

## قائمة تحقق

1. هل كل جدول يخص المستأجرين Tenants فيه عمود `tenant_id`، وهل يبدأ به المفتاح الأساسي والمفاتيح الأجنبية والقيود الفريدة؟
2. هل يحدد المستأجر Tenant مرة واحدة في بداية الطلب من مصدر موثوق، النطاق أو التوكن، مع فحص أن المستخدم عضو فيه؟
3. هل يمر السياق تلقائياً إلى قاعدة البيانات وال Cache والسجلات والطوابير والخدمات الأخرى، دون أن يمرره المطور يدوياً؟
4. هل يوجد حارس في قاعدة البيانات مثل RLS، والتطبيق يتصل بدور ليس مالكاً للجداول، و`SET LOCAL` داخل المعاملة؟
5. هل يوجد اختبار آلي يسجل الدخول بمستأجرين Tenants ويتأكد أن كل واحد لا يرى بيانات الآخر في كل صفحة وواجهة API؟
6. هل كل مفتاح Cache ومسار ملفات وفهرس بحث يحمل المستأجر Tenant؟
7. هل عندك حد للطلبات وحصص لكل مستأجر Tenant، وطوابير لا يحتكرها مستأجر واحد؟
8. هل تستطيع أن تجيب في دقيقة: كم يستهلك هذا المستأجر Tenant، وكم يكلفنا؟
9. هل الفروق بين العملاء في الإعدادات وأعلام الميزات، وليس في الكود؟
10. هل التسجيل والتصدير والحذف لكل مستأجر Tenant أدوات مؤتمتة جاهزة قبل أن يطلبها أول عميل؟

## الخلاصة

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

- المستأجر Tenant هو العميل المؤسسة وليس المستخدم، وتعدد المستأجرين يعني تطبيقاً واحداً يخدم كل العملاء، فيوفر التكلفة والتشغيل ويعطي الجميع إصداراً واحداً، وخطره الأكبر أن يرى مستأجر بيانات آخر.
- نماذج العزل الثلاثة Silo وPool وBridge قرار لكل طبقة ولكل باقة، وطبقة التحكم مشتركة دائماً.
- للبيانات ثلاثة خيارات: قاعدة لكل مستأجر Tenant لأقوى عزل وأعلى تكلفة، ومخطط لكل مستأجر وسطاً بينهما، وجداول مشتركة مع `tenant_id` لأعلى كثافة، وفي الأخيرة يكون `tenant_id` جزءاً من المفاتيح وRLS حارساً إضافياً.
- السياق يحدد مرة واحدة من النطاق أو التوكن مع فحص العضوية، ثم يمر إلى قاعدة البيانات وال Cache والسجلات والطوابير، وفي التجربة رأيت مفتاح Cache دون مستأجر Tenant يكشف أرقام acme لـ globex رغم أن RLS كان يعمل.
- الهوية في نظام متعدد المستأجرين Tenants تعني عضويات وأدواراً لكل مستأجر، ودخولاً موحداً وSCIM لكل عميل، والمستأجر في كل فحص صلاحية.
- الجار المزعج يعالج بحدود لكل مستأجر Tenant وطوابير عادلة ومراقبة وقياس لكل مستأجر، والقياس نفسه يخبرك بتكلفة كل عميل.
- ابدأ بالنموذج المشترك وحمل السياق من اليوم الأول، وأعط العزل لمن يحتاجه ويدفع ثمنه، وخطط ليوم يكبر فيه مستأجر Tenant واحد أكثر من سيرفر.

وفي [المقال الثاني من السلسلة](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/) نبني هذا عملياً: RLS كاملاً في PostgreSQL، وإنشاء المستأجر Tenant آلياً، وترحيل الجداول، والنسخ الاحتياطي والتصدير والحذف لكل مستأجر، ونقل مستأجر بين القواعد.

[![](https://arabroot.io/content/images/2026/10/multi-tenancy-practice-cover-cover.webp)دليلتطبيق SaaS متعدد المستأجرين عملياً مع Row-Level Securityنبني على PostgreSQL تطبيق SaaS يعزل كل مستأجر Tenant عن غيره بسياسات Row-Level Security، ونرى أين تتسرب البيانات رغم ذلك.](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/)[![](https://arabroot.io/content/images/2026/10/system-design-intro-cover-cover.webp)دليلمدخل إلى تصميم الأنظمة: من سيرفر واحد إلى ملايين المستخدمينكيف يكبر متجر إلكتروني من سيرفر واحد إلى نظام يخدم ملايين المستخدمين، وما الثمن الذي تدفعه في كل خطوة.](https://arabroot.io/articles/%D9%85%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D9%85%D9%86-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/db-design-cover-cover.webp)دليلتصميم قواعد البيانات: التطبيع Normalization ومتى تكسرهلماذا يفشل الجدول الواحد الكبير، وكيف تقسمه إلى جداول لا تناقض نفسها، وما الذي يتغير عندما تكبر البيانات.](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/)[![](https://arabroot.io/content/images/2026/10/connection-pooling-cover-cover.webp)دليلConnection Pooling: لماذا الاتصال بقاعدة البيانات مكلفلماذا تظهر رسالة too many clients عندما تزيد نسخ تطبيقك، وكيف يحل مجمع الاتصالات وPgBouncer هذه المشكلة.](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/sessions-cookies-jwt-cover-cover.webp)دليلكيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWTبروتوكول HTTP لا يتذكر أحداً، فكيف يبقى المستخدم مسجلاً بين الصفحات، ومتى تختار الجلسة على السيرفر ومتى تختار 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/)[![](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/)

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

- أكتوبر 2026: كتابة المقال واختبار التجربة على PostgreSQL 18.6 وValkey 9.1.2 مع Docker Engine 29.8.0.