بنية العروض التجريبية الخاصة
القرار المعماري
العرض ليس نسخة كاملة جديدة من المنصة ولا حساباً مشتركاً دائماً. هو tenant مؤقت داخل قاعدة الإنتاج يحمل is_demo=true ووقت انتهاء إلزامياً، وتحميه طبقات متراكبة من capability access وRLS والتحقق على مستوى API.
هذا يحقق تجربة واقعية ومخصصة دون تكلفة نشر stack مستقل لكل عميل محتمل، مع بقاء الإلغاء والتنظيف مركزيين.
تدفق الوصول
- ينشئ مشرف يملك
demos.manageرمزاً عشوائياً base64url عالي العشوائية. - ترسل لوحة الإدارة SHA-256 فقط إلى
create_demo_tenant، ثم تنشئ هويات المدير والمسافر والسائق. - تُشتق كلمات مرور الشخصيات بـHMAC من الرمز و
DEMO_ACCESS_SECRET؛ لا تخزن كلمات المرور الخام. - يوضع الرمز في URL fragment (
#access=...) حتى لا يظهر في طلب الصفحة أو سجل Traefik. - تقرأ صفحة bootstrap الجزء بعد اكتمال hydration، وتبقيه حتى يقبل الخادم
capability، ثم تحذفه من شريط العنوان عبر
history.replaceState. يمنع ذلك أن تفقد إعادة تركيب React للـeffect الرمز قبل اكتمال التحقق. - يتحقق الخادم من البصمة، الشركة، الإلغاء، ووقت الانتهاء، ثم يصدر جلسة الشخصية المطلوبة.
في تجربة المسافر تُثبت الجلسة في cookies من نوع
HttpOnlyفقط، وتُعاد ملاحة الصفحة كاملة حتى يتحققCustomerAuthProviderمنها من الخادم؛ لا تعاد رموز Supabase في JSON ولا تدخل JavaScript. - يعيد proxy الخاص بنطاق العرض التحقق من المنحة لكل طلب لاحق، بينما تفرض RLS نفس حدود الشركة على الاستعلامات المباشرة.
يرجع bootstrap رمز مجال ثابتاً مثل DEMO_ACCESS_INVALID أو
DEMO_PERSONAS_INCOMPLETE عند الفشل. تستخدم الواجهة الرمز لاختيار نص عربي أو
إنجليزي، ولا تعرض رسالة الاستثناء الخام. تحمل demo_config رسالة ترحيب عربية
وأخرى إنجليزية، بينما يحفظ اسم الشركة في name_ar وname_en.
يعرض /demo shell مستقلاً وثابتاً في أول render للخادم والمتصفح. لا يركب
غلاف تنقل العميل ولا CustomerAuthProvider قبل إنشاء جلسة المسافر، لذلك لا
تنتج صفحة التسليم طلبات /api/auth/customer/* مجهولة أو نصاً مختلفاً أثناء
hydration.
طبقات العزل
| الطبقة | الضمان |
|---|---|
| DNS وTraefik | نطاقات عرض مستقلة، TLS، وترويسات noindex وno-store |
| مخبأ المتصفح | Service Worker لا يخزن HTML أو RSC أو استجابات API الخاصة؛ يقتصر على الأصول العامة الثابة |
| حماية السعة | Traefik يحد bursts حسب IP، وبحث الرحلات يطبق حداً موزعاً، بينما يملك service role الداخلي ميزانية Kong منفصلة عن anon |
| تهيئة العرض | TOTP وصلاحية demos.manage، مع 10 عمليات لكل مشرف وIP خلال 15 دقيقة |
| تبادل الشخصية | 30 محاولة لكل IP خلال 15 دقيقة حتى لا يحجب فتح الشخصيات الثلاث شبكة مكتب كاملة |
| capability | 43 محرفاً تقريباً من entropy عشوائي؛ يفتح bootstrap الشخصيات فقط ولا يمنح مخزون الرحلات |
| جلسة الشخصية | JWT موثق يحمل demo_company_id مطابقاً للـcapability؛ لا يفتح tenant آخر أو بيانات حقيقية |
| API | البحث يحتفظ بجلسة المتصل ويختار مجالاً واحداً فقط: public أو internal أو demo مطابق |
| RLS | can_access_tenant يتطلب tenant مطابقاً ونشطاً وغير منتهٍ للعروض فقط |
| التجميع والمخبأ | المسارات الشائعة تجمع المجال المطابق فقط، ولا تخزن نتائج internal/demo في مخبأ عام |
| التسويق وSEO | landing routes وsitemap وملفات الشركات ومراجعاتها تقرأ شركات الإنتاج العامة فقط |
| أوامر الحجز | Journey quote وقفل المقاعد والحجز القديم تعيد «غير موجود» قبل لمس مخزون tenant غير مطابق |
| الآثار الخارجية | عناوين .invalid وأرقام +963900… وصندوق demo_outbox بدل الإرسال الحقيقي |
| دورة الحياة | الإلغاء/الانتهاء يمنعان الوصول فوراً، والتنظيف يحذف البيانات وهويات Auth |
الحساب العام أو التشغيلي العادي يستطيع الوصول إلى شركة إنتاج حقيقية وفق العضوية
والسياسات السابقة. أما هوية INTERNAL أو DEMO الصالحة فهي محدد حصري لعالم
المخزون وليست صلاحية إضافية فوق الإنتاج العام؛ لذلك يرفض can_access_tenant
عبور أي منها إلى شركة إنتاج حقيقية أيضاً.
فصل بيانات الإنتاج الداخلي
شركة QA الإنتاجية ليست عرضاً مؤقتاً، لذلك تحمل is_internal=true بدلاً من
is_demo. لا يمكن للحسابات العادية أو المجهولة اكتشاف رحلاتها أو فتحها أو حجز
مقاعدها. تمنح البذرة الحسابات الداخلية علامة موثوقة في app_metadata، ولا
تعتمد صلاحية الخادم على user_metadata القابل لتعديل المسافر. يمنع trigger
الأدوار العميلة من تغيير is_internal، ولا يسمح بجمع is_internal وis_demo
على الشركة نفسها. عند ترحيل هويات QA القديمة لا تكفي علامة user_metadata
وحدها؛ يشترط backfill سجلاً موثوقاً في إدارة المنصة أو عضوية الشركة أو السائق
أو حجزاً مرتبطاً بالشركة الداخلية قبل كتابة app_metadata.
يطبق search_journey_options مجالاً واحداً لكل طلب:
PUBLIC: شركات الإنتاج الحقيقية النشطة والمنشورة وغير الموقوفة فقط؛INTERNAL: شركات QA الداخلية النشطة والمنشورة وغير الموقوفة فقط؛DEMO: شركة العرض النشطة والمنشورة والمطابقة في JWT فقط، بشرط عدم انتهاء صلاحيتها.
رابط العرض ليس بديلاً عن حساب الشخصية. قبل قراءة أي Trip يجب أن تنشئ عملية
passenger_session جلسة المسافر الموقعة، وأن يطابق demo_company_id في JWT
الشركة التي قيدها رابط العرض. الرابط وحده أو حساب من مجال آخر يعيدان 401.
تطبق أوامر quote وقفل المقاعد والحجز المجال نفسه حتى لا يتحول تخمين UUID إلى
تجاوز لمسار البحث. تستبعد عروض public_companies وpublic_trips والمراجعات
والشهادات كلا المجالين الخاصين من الإحصاءات والتسويق. تطبق
popular_routes_aggregate المجال نفسه قبل التجميع، ويمنع API التخزين المشترك
لنتائج INTERNAL وDEMO كي لا تتحول طبقة CDN إلى قناة تسريب بين المجالات.
كما تبقى مسارات landing وsitemap وملف الشركة العام ومراجعات الشركة إسقاطات
عامة حصراً للشركات النشطة والمنشورة وغير الموقوفة؛ لا تتحول إلى استجابات خاصة عند وجود cookie، ولا تنشر معرفات شركات
QA أو العرض حتى عند تخمين المعرّف أو الـslug.
البيانات والمقياس
يبني seed_demo_company_world عالماً صغيراً ثابت الحجم: 4 حافلات، حتى 12 مساراً، 264 رحلة، 12 مسافراً، 100 حجز، وبيانات دعم ورسائل محدودة. ينشئ trigger مقيد بالشركات التجريبية مخطط bus_layouts.layout_document قياسياً لكل حافلة بحيث يساوي عدد عناصر المقاعد سعتها، ولا يمس حافلات الشركات الحقيقية. يمنع demo_tenant_cap() وجود أكثر من 50 tenant تجريبياً فعالاً، وتستخدم عملية الإنشاء advisory lock لمنع تجاوز السقف عند الطلبات المتزامنة.
إعادة الضبط تقفل tenant داخل المعاملة قبل الحذف وإعادة البناء، ويعيد trigger إنشاء مخططات المقاعد تلقائياً مع الحافلات الجديدة. تحذف الدالة الخاصة purge_demo_tenant_operational_world عالم التشغيل والـJourney والتذاكر والحضور والمالية والتسويات والجودة والإشعارات بترتيب الاعتماديات، لكنها ترفض أي Journey أو Journey Order متصل بـtenant آخر. لا يملك anon أو authenticated أو service_role صلاحية استدعائها مباشرة؛ تستدعيها فقط أوامر reset/reaper المصرح بها. يتطلب أي bypass لحماية السجل immutable علامة transaction-local مع تحقق مستقل أن الشركة is_demo=true. مهمة الحصاد تستخدم FOR UPDATE SKIP LOCKED حتى تستطيع أكثر من نسخة صيانة العمل بأمان دون حذف مزدوج.
يثبت اختبار الإنتاج المحمي tests/e2e/production/demo-lifecycle.spec.ts هذه
الحدود عبر دورة مؤقتة كاملة: تهيئة مخصصة، عزل مجهول، تبادل الشخصيات الثلاث، عدم
إعادة capability في قائمة الإدارة، عدم وجود أخطاء hydration أو console في مركز
العرض، إلغاء، حصاد، وتنظيف احتياطي. الاختبار دليل
على هذا المسار المحدد ولا يحول العرض إلى ضمان شامل لكل ميزة داخل الشخصيات.
حدود الثقة
SUPABASE_SERVICE_ROLE_KEYيبقى داخل خوادم Next.js ومهمة الصيانة فقط.DEMO_ACCESS_SECRETبطول 32 محرفاً على الأقل، مستقل عن JWT وservice-role وCRON_SECRET.CRON_SECRETيحمي/api/maintenanceويُرسل كـBearer من حاوية الصيانة الداخلية.- لا يجوز تفعيل
SHAMBUS_DEMO_ENVIRONMENT=trueعلى stack الإنتاج المشترك؛ الأمان لكل tenant يعتمد على الهوية التجريبية والعناوين المحجوزة. - نطاق
web-api.shambus.comيعرّض مسارات/api/فقط لتطبيقات الجوال، حتى تبقى صفحة النطاق العام في وضع «قريباً» دون كسر واجهات API.
الخصوصية والمراقبة
تُسجل عمليات الإنشاء والتمديد وإعادة الضبط والتدوير والإلغاء في سجل الإدارة. تخزن منحة الوصول اسم جهة الاتصال وبريدها الاختياريين فقط، وتُحذف مع tenant بعد الانتهاء. لا تضع بيانات عميل حقيقية داخل بيانات العرض.
للتشغيل اليومي راجع دليل دورة حياة العروض.