في Talentera (منصة التوظيف B2B من Bayt.com) قضيتُ بضع سنوات في تصميم سير عمل متعدّد المستأجرين يخدم مؤسسات وحكومات على codebase واحد. نموذج البيانات بدا جيداً في اليوم الأول — والأكثر إثارةً للدهشة — صمد لسنوات من متطلّبات الاستئجار المحدّدة. الحيلة كانت معاملة الهوية والتحكّم في الوصول كمحور تصميم من الدرجة الأولى، لا كاهتمام middleware مُدمَج بعد كتابة منطق العمل. هذا المنشور هو النسخة المختصرة لما أوصي به لأي مهندس يبني منصّة متعدّدة المستأجرين في 2026.

شكل المشكلة

التوظيف متعدّد المستأجرين هو تقريباً أسوأ حالة للتحكّم في الوصول:

  • فئات متعدّدة من المستأجرين. مؤسسات، شركات صغيرة، وزارات حكومية — نفس المنصّة، توقّعات مختلفة جذرياً حول ما تعنيه “العزل.”
  • موظّفون مشتركون. موظّفو توظيف يعملون لوكالة هذا الربع ووكالة مختلفة في الربع القادم. تاريخهم لا يمكن أن يتسرّب. بيانات اعتمادهم يجب أن تكون قابلة للنقل.
  • المرشّحون بدائية عابرة للمستأجرين. مرشّح يتقدّم لأدوار في عشر شركات. تلك الشركات لا يجب أن ترى تفاعلات بعضها مع ذاك المرشّح.
  • العقود الحكومية تأتي بمتطلّبات تدقيق. ليس “نحن نُسجّل الأشياء.” متطلّبات رسمية، مع تدقيقات دورية، مع تبعات قانونية للإغفال.

كل نقطة من تلك هي مشكلة هوية / تحكّم وصول أولاً. بقية المنصّة طبقة رفيعة فوقها.

النموذج الذي نجح

النموذج الذي شُحن وصمد بُني حول أربعة بدائل:

1. المستأجر ككيان أوّلي غير قابل للإهمال

كل صف في قاعدة البيانات كان له عمود مستأجر، مُفرَض بقيود على مستوى قاعدة البيانات. لا فلتر على مستوى التطبيق. لا “سنتذكّر إضافته.” Row-level security في Postgres (أو ما يعادلها في المخازن الأخرى). إن كتب مهندس استعلاماً لا يتضمّن فلتر المستأجر، قاعدة البيانات أثارت خطأً.

المقايضة كانت أن كل استعلام يبدو أكثر تفصيلاً بعض الشيء. الفائدة كانت أن خطأً في التطبيق لم يتسبّب قط في تسرّب عبر المستأجرين، لأن قاعدة البيانات رفضت خدمة الطلب.

2. الفاعل / العلاقة / المورد، لا المستخدم / الدور

النموذج الجاهز — المستخدمون لديهم أدوار، الأدوار لديها صلاحيات، الصلاحيات تحمي الموارد — مناسب للتطبيقات البسيطة. ينهار في سياق التوظيف متعدّد المستأجرين لأن دور المستخدم يعتمد على المستأجر الذي يعمل فيه. موظّف توظيف “مدير” في الوكالة A هو “ضيف” في الوكالة B، و”لا شيء” في الوكالة C.

النموذج الذي استخدمناه كان أقرب إلى أسلوب Zanzibar (قبل أن يصبح Zanzibar اسماً مألوفاً في مجتمع الهندسة): الفاعل لديه علاقة بمورد، يتوسّطها سياق المستأجر الحالي. استعلام واحد أجاب “هل يستطيع هذا الفاعل القيام بهذا الإجراء على هذا المورد في هذا السياق؟” والجواب حُسب من رسم بيانات العلاقة، لا من enum دور.

هذا التغيير الواحد أطلق العنان لعشرات الميزات التي كانت ستحتاج كوداً للصلاحيات المخصّصة في نموذج enum-دور.

3. هوية مشتركة، جلسة محدودة النطاق

المستخدمون كان لديهم هوية واحدة — تسجيل دخولهم. حين يُوثَّقون، يحصلون على رمز جلسة محدود النطاق لمستأجر محدّد. تبديل المستأجرين كان يعني إصدار جلسة جديدة، لا تغيير الموجودة. الجلسة حملت معرّف المستأجر، علاقات الفاعل ذات الصلة بذاك المستأجر، والمعرّف المرئي للتدقيق للجلسة ذاتها.

هذا يعني أن كل إجراء مُدقَّق كان قابلاً للتتبّع بشكل تافه إلى (فاعل، مستأجر، جلسة، طابع زمني) — وأسئلة “من فعل ماذا لمن” من المدقّقين أصبحت جواب استعلام واحد.

4. التدقيق كحدث domain مُضاف فقط

كل طفرة في النظام بعثت حدث domain إلى سجل تدقيق مُضاف فقط. سجل التدقيق كان جزءاً من الدرجة الأولى من مخطّط التطبيق، لا إضافة تشغيلية. حين سأل مدقّق حكومي “أرنِ جميع مشاهدات السيرة الذاتية بواسطة هذا الموظّف في مارس”، الجواب كان استعلاماً، لا تمرين متعدّد أسابيع في ترابط السجلات.

بناء هذا من اليوم الأول أرخص بـ3× من تركيبه لاحقاً. أعرف لأنني فعلتُ كليهما.

الإخفاقات

شيئان أخطأتُ فيهما:

1. النسخة الأولى من توريث الدور كانت مسطّحة جداً. كان عندنا طبقة مدير-مستخدم-مساهم وهذا كان كل شيء. أرادت المؤسسات مدراء فرعيين. كان علينا إضافة تكوين الدور بعد الحقيقة، مما يعني فكّ تشابك مجموعة من الافتراضات بأن “المدير يعني كل هذه الأشياء.” الخطوة الصحيحة كانت بناء التكوين من اليوم الأول — لكنني لم أرَ الحاجة حتى العميل رقم 20.

2. نموذج تعدّد المستأجرين لم يمتدّ بنظافة إلى التحليلات. للاستعلامات التعاملية، كان تحديد نطاق المستأجر على مستوى الصف محكماً. للأحمال BI / reporting، أصبح مكلفاً بسرعة. انتهى بنا الأمر بـpipeline تحليلات منفصل يُفرض فيه حدّ المستأجر على مستوى pipeline، لا مستوى الاستعلام. نجح ذلك — لكنه كان كثيراً من السباكة لبنائها مرتين، وكنتُ سأتعامل مع الأمر بشكل مختلف لو بدأتُ من جديد.

ما كنتُ سأفعله بشكل مختلف في 2026

بعد أربع سنوات، بدءاً من الصفر، كنتُ:

  • أصل إلى OPA (Open Policy Agent) أو محرّك سياسة مشابه من اليوم الأول، بدلاً من بناء محرّك تقييم الصلاحيات بنفسي. أرخص، مُدقَّق، مُختبَر جيداً، والنموذج المفاهيمي نفسه.
  • أستخدم Postgres row-level security أو حدّاً صارماً مكافئاً. لا “التطبيق يُفلتر في المنطق.” يجب أن يكون المحيط على طبقة قاعدة البيانات.
  • أختار مخطّط حدث تدقيق منظّم قبل كتابة أول endpoint طفرة. كل حدث هو (فاعل، موضوع، إجراء، مورد، مستأجر، طابع زمني، نتيجة، diff). إن كانت ميزة جديدة تحتاج شكل حدث لا يدعمه المخطّط، فتلك محادثة مخطّط، لا “فقط سجّل شيئاً.”
  • أستخدم Open ID Connect + مزوّد هوية مخصّص بدلاً من تنفيذ auth. Pocket ID، Keycloak، Auth0، Ory — الخيارات الجيّدة أرخص من تكلفة DIY.

النمط الذي يعمّم

لا شيء من هذا خاصّ بالتوظيف. كل SaaS للأعمال، كل منصّة متعدّدة المستأجرين، كل نظام مجاور للحكومة عنده نفس الشكل. السبب في كونها فوضى في معظمها هو أن الهوية والتحكّم في الوصول يُعامَلان كـmiddleware — مُدمَجان، غير كافيَي التحديد، لا يُعطيان أولوية أبداً. تلك التي تتقدّم بالعمر بشكل جيّد تعامل الهوية كبُعد منتج: جزء من نموذج النطاق، جزء من مراجعات التصميم، جزء من خارطة الطريق.

إن كنتَ تبني منصّة متعدّدة المستأجرين الآن: نموذج الهوية هو أكثر اختيار تصميم يحمل الثقل ستتخذه في السنة الأولى. لا يمكنك تركيبه لاحقاً برخص. أصِبه. أنفق الوقت. اقرأ ورقة Zanzibar. تحدّث إلى من فعلوا ذلك. إنه يتراكم.