حين تبيع نظامك بالاشتراك لأكثر من شركة، يصبح لكل عميل ركن خاص داخل منتج واحد. والمكان الذي تضع فيه هذا الركن، سواء كان صفوفًا في جدول مشترك أو سكيما مستقلة أو قاعدة بيانات كاملة، يحدّد بعد ذلك كم تدفع للخوادم، وكم تستغرق الترقية الواحدة، وهل تستطيع أصلًا قبول عميل يشترط بقاء بياناته داخل بلده.
هذا ما يُقصد بتطبيق SaaS متعدّد المستأجرين (multi-tenant): نسخة واحدة من البرنامج تخدم عدّة شركات، كل واحدة منها «مستأجر» يرى بياناته وحدها. والسؤال الهندسي الوحيد الذي يستحق أن تبدأ منه هو أين ترسم الحدّ بين مستأجر وآخر.
كثيرون يحسمون هذا القرار بسؤال واحد: «أي النماذج أكثر أمانًا؟». وهو سؤال لا يقود إلى إجابة، لأن النماذج الثلاثة كلها آمنة إذا نُفّذت بعناية، وكلها قابلة للاختراق إذا أُهملت. الأسئلة التي تفرز بينها فعلًا أربعة، وكلها تشغيلية:
- إذا طلب عميل واحد استرجاع بياناته إلى ما قبل ساعتين، كم يستغرق ذلك، وماذا يحدث لبقية العملاء أثناءه؟
- إذا أضفت عمودًا إلى جدول أساسي، كم مرة ستُنفَّذ العملية، وكم دقيقة يتوقف فيها المنتج؟
- ما الذي يحدث لو نسي مطوّر شرط tenant_id في استعلام واحد؟
- إذا استهلك مستأجر واحد الخادم، من يشعر بذلك غيره؟
هذا المقال يجيب عن الأربعة، ويربط الإجابة بما تنصّ عليه وثائق PostgreSQL وMySQL وLaravel نفسها، لا بانطباعات عامة.
النماذج الثلاثة، وما يعنيه كل منها يوم التشغيل
| جدول مشترك بعمود tenant_id | سكيما لكل مستأجر | قاعدة بيانات لكل مستأجر | |
|---|---|---|---|
| كلفة البنية التحتية | الأدنى؛ خادم واحد يخدم الجميع | متوسطة؛ خادم واحد بعدد أكبر من الكائنات | الأعلى؛ اتصالات ونسخ وصيانة لكل قاعدة |
| استرجاع مستأجر واحد | جراحة على مستوى الصفوف | ممكن بتصدير السكيما، مع تحفّظ مهم | استرجاع ملف واحد |
| الهجرة | عملية واحدة | عملية مكرّرة بعدد المستأجرين | عملية مكرّرة بعدد القواعد |
| تقارير تجمع كل العملاء | استعلام عادي | تحتاج تجميعًا صريحًا | تحتاج خطّ بيانات منفصلًا |
| الجار الصاخب | لا يوجد حاجز | حاجز محدود | حاجز حقيقي |
| الاستضافة في بلد بعينه لعميل بعينه | غير ممكن دون فصله | غير ممكن دون فصله | ممكن لكل عميل على حدة |
الجدول المشترك بعمود tenant_id
كل الشركات في نفس الجداول، ويميّز بينها عمود واحد. هذا النموذج أرخص ما يمكن تشغيله، وأسهل ما يمكن ترقيته: تنفّذ الهجرة مرة واحدة فتصل إلى الجميع، وتكتب تقريرًا يقارن استهلاك كل العملاء باستعلام عادي.
ثمنه أن العزل صار مسؤولية كل سطر كود تكتبه. والأخطر أن الاسترجاع الفردي يتحوّل إلى عملية يدوية: لا يوجد ملف اسمه «بيانات هذا العميل»، وإنما صفوف متناثرة في عشرين جدولًا بينها مفاتيح أجنبية وتسلسل محدّد للحذف والإدراج. وأول مرة يطلب فيها عميل استرجاعًا بعد خطأ إدخال جماعي، تكتشف أنك لم تكتب هذه الأداة بعد.
سكيما لكل مستأجر
قاعدة بيانات واحدة، وداخلها مجموعة كاملة من الجداول لكل عميل. توثيق PostgreSQL يشبّه السكيمات بالمجلدات، مع فارق أنها لا تتداخل، وأن الوصول إلى سكيما لا يملكها المستخدم يستلزم منحه صلاحية USAGE عليها. هذا يعطيك حاجزًا يفهمه الخادم نفسه: حساب مربوط بسكيما واحدة لا يرى غيرها حتى لو أخطأ الكود.
والنسخ الاحتياطي يبدو للوهلة الأولى محلولًا، لأن pg_dump -n يصدّر سكيما بعينها. لكن توثيق الأداة يضيف تحفّظًا يستحق أن يُقرأ قبل بناء الخطة كلها عليه: عند استخدام -n لا يحاول pg_dump تصدير أي كائنات أخرى قد تعتمد عليها السكيما المختارة، ولذلك «لا يوجد ضمان أن نتيجة تصدير سكيما بعينها يمكن استرجاعها وحدها في قاعدة نظيفة». الأنواع المعرَّفة والدوال المشتركة والامتدادات تعيش خارج السكيما عادةً. فإن اخترت هذا النموذج، اجعل أول اختبار تكتبه استرجاع سكيما واحدة في قاعدة فارغة تمامًا، لا في نسخة من الإنتاج.
المشكلة الثانية اسمها الهجرة. توثيق ALTER TABLE ينصّ على أن الأمر يأخذ قفل ACCESS EXCLUSIVE ما لم يُذكر خلاف ذلك لصيغة بعينها، وجدول الأقفال يوضّح أن هذا القفل وحده هو الذي يحجب حتى SELECT. أي أن تعديل جدول في سكيما مستأجر يجمّد ذلك المستأجر لحظة التنفيذ. عند عشرين عميلًا هذا احتمال مقبول، وعند ألف عميل تحتاج خطة تشغّل الهجرات على دفعات وتتحمّل فشل بعضها في المنتصف. ولحسن الحظ ليست كل التعديلات متساوية: إضافة مفتاح أجنبي مثلًا تكتفي بقفل SHARE ROW EXCLUSIVE وفق المصدر نفسه.
وإن كنت على MySQL فالنموذج الأوسط هذا غير موجود عندك أصلًا: توثيق CREATE DATABASE يذكر صراحةً أن CREATE SCHEMA مرادف لـCREATE DATABASE. فأنت تنتقل من الجدول المشترك إلى قاعدة لكل مستأجر مباشرة.
قاعدة بيانات لكل مستأجر
أقوى عزل، وأوضح نسخ احتياطي، وأسهل إجابة حين يسألك عميل: «أين بياناتي بالضبط؟». وله ثمن لا يظهر في المقارنات النظرية: الاتصالات.
القيمة الافتراضية لـmax_connections في PostgreSQL، كما يذكر التوثيق، هي مئة اتصال عادةً، ولا يمكن تغييرها إلا عند إقلاع الخادم. ولأن مجمّع الاتصالات (connection pool) يُبنى عادةً لكل قاعدة على حدة، فإن مئة عميل بحدّ أدنى معقول من الاتصالات لكل واحد يستهلكون الرقم قبل أن يدخل مستخدم حقيقي واحد. الحل عمليًا مجمّع وسيط يعيد استخدام الاتصالات عبر القواعد، وهذا مكوّن إضافي في بنيتك التحتية له إعداداته وأعطاله.
وتبقى الهجرة مكرّرة كما في نموذج السكيما، ويزيد عليها أن التقرير الذي يجمع كل العملاء صار مهمة نقل بيانات إلى مستودع منفصل، لا استعلامًا.
الخطر الأول: استعلام واحد نسي tenant_id
في النموذج المشترك، تسريب البيانات بين المستأجرين لا يحتاج مهاجمًا. يكفي تقرير جديد كتبه مطوّر متعجّل باستعلام خام، أو مهمة خلفية تعالج «كل الطلبات المتأخرة»، أو صفحة تصدير تستقبل معرّفًا من الرابط دون أن تتحقق من صاحبه.
الاعتماد على انتباه المطوّر ليس ضمانة، لأنه يفترض أن كل من سيكتب استعلامًا في هذا النظام خلال السنوات القادمة سيتذكّر القاعدة نفسها. الضمانة الهيكلية أن ترفض القاعدة أو الإطار الاستعلام بدل أن ينجح ناقصًا.
ما يمنحه الإطار: نطاق عام في Laravel
توثيق Laravel يصف النطاق العام (global scope) بأنه يضيف قيودًا إلى كل استعلامات نموذج معيّن. هذا حلّ نظيف وسريع، وحدوده منصوص عليها في الصفحة نفسها: withoutGlobalScope تزيل نطاقًا بعينه، وwithoutGlobalScopes تزيلها كلها. وفوق ذلك، النطاق مرتبط بالنموذج، فأي استعلام يُكتب عبر منشئ الاستعلامات مباشرة أو بـSQL خام لا يمرّ عليه أصلًا.
بعبارة أخرى: النطاق العام يحمي من النسيان، ولا يحمي من التجاوز. وهو كافٍ لمنتج صغير بفريق واحد، وغير كافٍ لمنتج يمسّ بيانات صحية أو مالية.
ما تمنحه القاعدة: أمان الصفوف في PostgreSQL
سياسات أمان الصفوف تنقل الشرط إلى الخادم. بعد ALTER TABLE … ENABLE ROW LEVEL SECURITY، يصبح كل وصول عادي للجدول مشروطًا بسياسة، وإن لم توجد سياسة «تُستخدم سياسة المنع الافتراضي، ما يعني ألا يظهر أي صف ولا يمكن تعديله». وهذا سلوك تريده بالضبط: الوضع الافتراضي لا يرى شيئًا.
النمط المعتاد أن تحمل الجلسة هوية المستأجر في معامل ثنائي الاسم مثل app.tenant_id. وتوثيق الخيارات المخصّصة يذكر أن PostgreSQL يقبل ضبط أي معامل من جزأين ويعامله كعنصر نائب حتى لو لم يعرّفه امتداد. أما الجزء الحاسم فهو معامل is_local في الدالة set_config: حين يكون true، لا تسري القيمة الجديدة إلا داخل المعاملة الحالية. مع مجمّع اتصالات يعيد توزيع الاتصال نفسه على طلبات متتابعة، هذا الفارق هو ما يمنع هوية مستأجر من التسرّب إلى طلب المستأجر التالي.
وهناك ثلاثة مطبّات موثّقة يقع فيها من ينفّذ هذا للمرة الأولى:
- مالك الجدول يتجاوز السياسة. التوثيق ينصّ على أن المستخدم الخارق وأي دور يحمل صفة BYPASSRLS يتجاوزان أمان الصفوف دائمًا، وأن مالك الجدول يتجاوزها عادةً ما لم يُخضع نفسه لها بـALTER TABLE … FORCE ROW LEVEL SECURITY. وبما أن تطبيقك يتصل غالبًا بالحساب الذي أنشأ الجداول، فإن تفعيل السياسات دون هذه الخطوة يعطيك شعورًا بالأمان بلا أمان.
- السياسة التقييدية وحدها تمنع كل شيء. توثيق CREATE POLICY يشرح أن السياسات المتساهلة تُدمج بـ«أو» والتقييدية بـ«و»، ثم يضيف جملة صريحة: إن لم توجد سياسة متساهلة واحدة، يُرفض الوصول. من يكتب سياسة المستأجر بصيغة RESTRICTIVE وحدها يجد الجدول فارغًا ولا يفهم السبب.
- قيود التفرّد تتحدّث. فحوص التكامل المرجعي، أي المفاتيح الفريدة والأساسية والأجنبية، تتجاوز أمان الصفوف دائمًا حفاظًا على سلامة البيانات، والتوثيق يحذّر صراحةً من «قنوات خفية» تسرّب معلومات عبرها. عمليًا: قيد تفرّد على البريد الإلكتروني في جدول مشترك يجعل رسالة الخطأ تخبر عميلك أن هذا البريد مستخدم عند عميل آخر. اجعل التفرّد مركّبًا على (tenant_id, email) بدل email وحده.
فريق سنابل يوفر لك دراسة فنية مخصصة لتحديد التقنيات المناسبة وتكلفة التنفيذ الدقيقة.
وحين تأخذ نسخة احتياطية من جدول عليه سياسات، فعّل row_security = off. التوثيق يوضّح أن هذا لا يتجاوز السياسات، وإنما يرفع خطأً إن كانت نتيجة الاستعلام ستُفلتر، فتكتشف المشكلة وقت النسخ بدل أن تكتشفها وقت الاسترجاع.
ولمن يعمل على MySQL: لا يوجد مكافئ على مستوى الصف. توثيق GRANT يبيّن أن أدقّ مستوى تُمنح عنده الصلاحية على البيانات هو العمود داخل جدول. فالحاجز الذي يفهمه خادم MySQL هو قاعدة البيانات أو الجدول أو العمود، وليس الصف. وهذا سبب عملي وجيه لاختيار قاعدة لكل مستأجر على MySQL، أو لبناء المنتج على PostgreSQL من البداية إن كان العزل الصارم شرطًا في عقودك. قرار مشابه في طبقة التطبيق شرحناه في متى تختار React ومتى تختار Laravel.
حين يفرض الامتثال النموذج عليك
أحيانًا لا تختار. يأتي عميل مؤسسي، أو جهة تعمل في قطاع منظَّم، ويضع في كرّاسة الشروط بندًا واحدًا: بياناتنا تبقى داخل البلد، أو على بنية منفصلة.
في مصر، تشترط المادة 14 من قانون حماية البيانات الشخصية ترخيصًا من المركز لتخزين البيانات الشخصية في دولة أجنبية، وكلمة «تخزينها» تشمل وضع قاعدة بياناتك على خادم خارج البلاد. وفي السعودية، تقيّد المادة 29 من نظام حماية البيانات الشخصية النقل خارج المملكة بشروط، وتفصّل لائحة النقل الضمانات المقبولة. التفاصيل والمهل والغرامات تناولناها في حماية بيانات العملاء في مصر والسعودية، وأثرها على اختيار المزوّد وموقع الخادم في كيف تختار شركة استضافة لموقعك.
ما يعنينا هنا نتيجة واحدة: العميل الذي يلزمه بقاء بياناته في بلده لا يمكن أن يعيش في جدولك المشترك المستضاف في فرانكفورت. لذلك حتى لو بدأت بالجدول المشترك، وهي بداية صحيحة لأغلب المنتجات، فصمّمه بحيث يمكن انتزاع مستأجر واحد منه ونقله إلى قاعدة مستقلة دون إعادة كتابة التطبيق. عمليًا يعني ذلك أن تمرّ كل الوصولات على طبقة واحدة تعرف كيف تحلّ اتصال المستأجر، وألا تفترض في أي مكان أن كل العملاء في نفس القاعدة.
وهذه البنية الهجينة هي ما تنتهي إليه معظم المنتجات الناضجة: غالبية المستأجرين في المشترك، وقلة على قواعد خاصة بعقود خاصة وتسعير يعكس كلفتها. مسألة الاحتفاظ والحذف والمدد القانونية للنسخ تكمل الصورة، وهي في النسخ الاحتياطي للمواقع.
الجار الصاخب
مستأجر واحد يرفع ملفًا بمليون صف، أو يشغّل تقريرًا يمسح كل تاريخه، فيبطؤ المنتج عند الجميع. هذه ليست مشكلة كود، وإنما مشكلة تقاسم موارد، وتُعالج بمقابض لها أسماء محدّدة.
أول ما تحتاجه القياس. الامتداد pg_stat_statements يتتبّع إحصاءات تخطيط الجمل وتنفيذها، ويلزم لتفعيله إضافته إلى shared_preload_libraries وإعادة تشغيل الخادم، وسقفه الافتراضي 5000 جملة متمايزة. بدونه ستظل تخمّن أي مستأجر هو السبب.
ثم التحكّم. إعدادات الجلسة تتيح statement_timeout لإجهاض أي جملة تتجاوز مدة محدّدة، وidle_in_transaction_session_timeout لإنهاء جلسة تركت معاملة مفتوحة وجلست خاملة تحتجز الأقفال. والتوثيق ينبّه إلى أن ضبط statement_timeout في postgresql.conf غير مستحسن لأنه يطال كل الجلسات، وهو بالضبط سبب ضبطه لكل دور أو لكل نوع اتصال بدلًا من ذلك. ويضاف إلى ذلك حدّ اتصالات لكل دور عبر CONNECTION LIMIT، وقيمته الافتراضية -1 أي بلا حدّ، مع تحفّظين منصوص عليهما: يُطبَّق تقريبيًا فقط، ولا يُطبَّق أبدًا على المستخدم الخارق. ويمكن وضع الحدّ نفسه على مستوى قاعدة البيانات في نموذج قاعدة لكل مستأجر.
وMySQL أوسع في هذا الباب تحديدًا: تسمح بحدود لكل حساب على عدد الاستعلامات والتحديثات والاتصالات في الساعة، وعلى عدد الاتصالات المتزامنة، تُضبط في جملة CREATE USER … WITH أو ALTER USER … WITH، وقيمتها الافتراضية صفر أي بلا حدّ. وللدقة عند التخطيط: التوثيق يذكر أن العدّ يجري لكل حساب لا لكل عميل، وأن العدّادات تبدأ من الصفر عند كل إقلاع للخادم.
يبقى بند لا تغطّيه قاعدة البيانات: الطوابير. مهمة ثقيلة لمستأجر كبير تستطيع أن تعطّل إشعارات كل المستأجرين إن أجلست الجميع في طابور واحد. افصل الطوابير الطويلة عن القصيرة، وضع سقفًا لعدد المهام المتزامنة لكل مستأجر.
الاشتراكات والفوترة: بيانات ليست ملكًا للمستأجر
الباقات والحدود تبدو مسألة واجهة، وهي في الحقيقة مسألة إنفاذ. أي حدّ تعلنه (عدد المستخدمين، مساحة الملفات، عدد الرسائل شهريًا) يجب أن يُفحص في الخادم قبل تنفيذ العملية، لا في الواجهة بعد إتمامها. والترقية والتخفيض يحتاجان قرارًا مكتوبًا قبل البرمجة: ماذا يحدث للمستخدم رقم 11 حين يهبط العميل من باقة تسمح بعشرين إلى باقة تسمح بعشرة؟ تعطيله أو منع الهبوط قراران مشروعان، وعدم اتخاذ أيهما يترك لك بيانات متناقضة.
أما التفصيلة التي تُغفل غالبًا فهي أن بيانات الفوترة وعدّادات الاستهلاك وسجلّ التدقيق ليست بيانات المستأجر، وإنما بيانات منصّتك عنه. إن وضعتها داخل قاعدته أو سكيماه، فاسترجاعه إلى ما قبل يومين يمحو فواتيره وسجلّ تدقيقك معًا، ويعطيه أيضًا سلطة على أدلة قد تحتاجها في نزاع. ضعها في قاعدة المنصة، واربطها بمعرّف المستأجر فقط.
وإن كنت تبيع اشتراكات لعملاء في السعودية، فأنت مكلَّف بالفوترة الإلكترونية عن اشتراكاتك أنت. صفحة مراحل التطبيق لدى هيئة الزكاة والضريبة والجمارك تذكر أن المرحلة الأولى نافذة منذ 4 ديسمبر 2021، وأن مرحلة الربط والتكامل نافذة منذ 1 يناير 2023 وتُطبَّق على مجموعات يُشعَر المكلفون بها قبل ستة أشهر على الأقل (اطُّلع على الصفحة في 22 سبتمبر 2026). ما يتغيّر داخل النظام عند الربط شرحناه في المرحلة الثانية من الفوترة الإلكترونية، ونظيره المصري في ربط نظامك بالفاتورة الإلكترونية المصرية.
التخصيص لكل مستأجر، وأين ينتهي
الشعار والألوان والنطاق الفرعي تخصيص رخيص: إعدادات تُقرأ عند الإقلاع، ولا تمسّ بنية البيانات. والنطاق المخصّص الكامل (app.client.com) أثقل قليلًا، لأنه يضيف خطوة يؤدّيها العميل نفسه في إعدادات نطاقه، وخطوة إصدار شهادة في طرفك، وكلتاهما تدخلان مسار تهيئة العميل الجديد ويجب أن تُختبرا كأي ميزة.
الحقول الإضافية أصعب. إضافة عمود لعميل واحد في جدول مشترك تعني ALTER TABLE على جدول يستخدمه الجميع، وقد قرأنا أعلاه ما يعنيه ذلك من قفل. البديل الشائع عمود jsonb يحمل حقول العميل الحرة، وتوثيق أنواع JSON يذكر أن فئة معاملات GIN الافتراضية لـjsonb تدعم معاملات وجود المفتاح والاحتواء ومطابقة jsonpath، أي أن البحث في هذه الحقول يظل مفهرسًا. ثمنه أن قاعدة البيانات لم تعد تتحقّق من أنواع هذه الحقول نيابة عنك.
والحدّ الذي يجب أن تعرفه سلفًا هو منطق العمل. حين يطلب عميلان قاعدتي احتساب مختلفتين في نفس الشاشة، تكون قد بدأت صيانة منتجين بفريق واحد. هنا يعود السؤال إلى أصله: هل تبني منتجًا أم نظامًا مخصّصًا لشركة واحدة؟ الفرق بينهما وكلفته تناولناهما في أنظمة جاهزة أم مخصّصة وتكلفة نظام ERP مخصّص.
كيف تحسم القرار في جلسة واحدة
اجلس مع فريقك وأجب عن ستة أسئلة مكتوبة:
- هل في عقودك اليوم، أو في خطّتك لسنة قادمة، عميل يشترط بيانات داخل بلده أو على بنية منفصلة؟
- كم مستأجرًا تتوقّع بعد سنتين: عشرات أم آلاف؟
- ما حساسية البيانات: طلبات مطعم أم سجلات مرضى أم قيود محاسبية؟
- كم مرة تنوي تعديل بنية قاعدة البيانات شهريًا في السنة الأولى؟
- هل تحتاج تقارير تجمع كل المستأجرين لتشغيل عملك أنت؟
- من سيقوم بالاسترجاع الفردي حين يُطلب، وبأي أداة؟
الأنماط التي تخرج من هذه الأسئلة واضحة عادةً. آلاف المستأجرين الصغار ببيانات عادية وتعديلات متكرّرة على البنية: جدول مشترك مع أمان صفوف مفعَّل من اليوم الأول. عشرات المستأجرين ببيانات حساسة وشرط عزل في العقد: قاعدة لكل مستأجر. ما بينهما: ابدأ بالمشترك، واكتب من اليوم الأول طبقة تحلّ اتصال المستأجر، وأداة تنقل مستأجرًا واحدًا إلى قاعدة مستقلة. أرخص ما في القرار أن تحتفظ بخيار تغييره.
وإن كان أمامك نظام قائم تفكّر في تحويله إلى منتج يخدم عدّة شركات، أو منتج SaaS تبدأه من الصفر، اعرض الفكرة على فريق سنابل: يصلك تصوّر أوّلي للنطاق، وتقدير للمدة والتكلفة، في غضون يوم واحد، بلا مقابل وبلا ارتباط.
المصادر
- سياسات أمان الصفوف وCREATE POLICY والسكيمات وpg_dump وALTER TABLE والأقفال الصريحة وإعدادات الاتصالات وإعدادات الجلسة وCREATE ROLE وALTER DATABASE والخيارات المخصّصة ودوال إدارة النظام وpg_stat_statements وأنواع JSON. توثيق PostgreSQL، الإصدار 18.
- CREATE DATABASE وGRANT وحدود موارد الحسابات. دليل MySQL المرجعي، الإصدار 8.4.
- Eloquent: النطاقات العامة. توثيق Laravel، الإصدار 13.
- مراحل تطبيق الفوترة الإلكترونية. هيئة الزكاة والضريبة والجمارك، آخر تحديث 9 أغسطس 2026.
اطُّلع على جميع الروابط في 22 سبتمبر 2026.
الأسئلة الشائعة
ما الفرق بين تعدّد المستأجرين وتشغيل نسخة مستقلة لكل عميل؟
في تعدّد المستأجرين تعمل نسخة واحدة من الكود وتخدم كل العملاء، فتنشر تحديثًا واحدًا يصل إلى الجميع. وفي النسخة المستقلة لكل عميل (single-tenant) تُشغّل تطبيقًا وقاعدة بيانات منفصلين لكل عقد، فتنال عزلًا كاملًا مقابل عملية نشر وصيانة مكرّرة بعدد عملائك. الفارق تشغيلي في المقام الأول: النموذج الأول يوفّر جهد التشغيل ويطالبك بعزل صارم داخل البيانات، والثاني يعفيك من ذلك ويحمّلك كلفة تشغيل تنمو مع كل عميل جديد.
كيف أتأكد أن عزل البيانات يعمل فعلًا قبل الإطلاق؟
اكتب اختبارات تحاول عمدًا الوصول إلى بيانات مستأجر آخر: طلب بمعرّف يخصّ مستأجرًا ثانيًا، وتقرير يُنفَّذ بلا هوية مستأجر في الجلسة، وتصدير يستقبل معرّفًا من الرابط. الاختبار الناجح هو الذي يفشل الطلب فيه أو يعود بصفر صفوف، لا الذي يعيد البيانات الصحيحة. وإن كنت تستخدم سياسات أمان الصفوف في PostgreSQL، فأضف اختبارًا يتصل بحساب التطبيق نفسه لا بحساب المدير، لأن مالك الجدول يتجاوز السياسات ما لم تُفعّل FORCE ROW LEVEL SECURITY.
هل أحتاج نطاقًا فرعيًا لكل مستأجر منذ البداية؟
لا. يكفي في البداية مسار داخل التطبيق يحدّد المستأجر بعد تسجيل الدخول. النطاق الفرعي مفيد حين يصبح العنوان جزءًا من هوية العميل أو حين تفصل الجلسات بينه وبين غيره، والنطاق المخصّص الكامل يأتي عادةً كطلب من عملاء أكبر. المهم أن تمرّ هوية المستأجر من مكان واحد في الكود، حتى يكون تغيير طريقة التعرّف عليه لاحقًا تعديلًا في طبقة واحدة.
هل تغني سياسات أمان الصفوف عن التحقق من الصلاحيات داخل التطبيق؟
لا، لأنهما يجيبان عن سؤالين مختلفين. السياسات تجيب عن «أي الصفوف تخصّ هذا المستأجر»، بينما صلاحيات التطبيق تجيب عن «ماذا يحقّ لهذا المستخدم داخل مستأجره»: من يرى الرواتب، ومن يعتمد الخصم، ومن يحذف. تحتاج الاثنتين معًا، والسياسات هي الشبكة الأخيرة التي تلتقط أخطاء الطبقة الأولى.



