0%
balqalam Logo
بالقلم
المميزاتالمجتمعالتسعيرالوثائق
تسجيل الدخول
  • مقدمة
  • العرض
  • MVP
  • خارطة الطريق
  • متطلبات المنتج
  • ابدأ الآن
  • البيئة المحلية
  • البنية المعمارية
  • الهيكل
  • الأنماط
  • الحزمة التقنية
  • قاعدة البيانات
  • تعدّد المستأجرين
  • الفهرس (Catalog)
  • العمل دون اتصال
  • المصادقة
  • التهيئة (Onboarding)
  • تدفق التكامل (Integration Flow)
  • التهيئة (Provision)
  • القبول
  • الحضور
  • الامتثال التنظيمي
  • الملف الشخصي
  • الاجتماعات
  • إدارة الرسوم
  • الفاتورة
  • التدويل
  • الترجمة
  • دليل الترجمة
  • أمان قاعدة البيانات
  • المساهمة
  • هَوْغوارتس
  • عرض حي
  • الإلهام
المبيعات والانتشار
  • المبيعات
  • الذهاب إلى السوق
  • التسويق
  • القبول — تسليط الضوء
  • البرنامج التجريبي
  • العملاء المحتملون
  • العرض التجاري
  • قوالب التواصل
  • دراسة الحالة
  • المنافسون
  • نموذج العمل
  • الاقتصاد المشترك
  • الجذب والنمو
التمويل والشراكات
  • الحصول على الدعم
  • عرض المستثمر
  • غرفة البيانات
  • المستثمرون
  • المُسرِّعات
  • الحاضنات
  • المنح
  • الرعاة
  • الشركاء
  • المسابقات والهاكاثونات
  • الجامعات ومراكز التدريب

تعدّد المستأجرين

السابقالتالي

توجيه النطاقات الفرعية، حلّ سياق المستأجر، وعزل قاعدة البيانات عبر تحديد نطاق `schoolId`.

Hogwarts منصّة SaaS متعدّدة المستأجرين: كل مدرسة تعمل على نطاقها الفرعي الخاص (school.databayt.org) مع عزل كامل للبيانات عبر تحديد نطاق schoolId. تغطّي هذه الصفحة المسار من edge middleware حتى استعلام قاعدة البيانات.

للتوثّق، OAuth، وRBAC راجع authentication، oauth، وauthorization.

مسار الطلب

طلب إلى school.databayt.org/dashboard يمرّ بأربع طبقات:

  1. Edge middleware يتجاوز الملفات الثابتة، يكتشف اللغة والنطاق الفرعي، يفكّ تشفير JWT بشكل خفيف لـRBAC، ويعيد كتابة الـURL إلى /en/s/school/dashboard. ترويسة x-subdomain تُضبَط للمستهلكين في الطبقات اللاحقة.
  2. سياق المستأجر يحلّ schoolId القانوني بالأولوية: cookie الانتحال ← ترويسة x-subdomain ← session JWT.
  3. مكوّن الخادم / الإجراء يستخدم getTenantContext() ويُدرج schoolId في كل استعلام قاعدة بيانات.
  4. قاعدة البيانات تفرض أمان المستأجر عبر قيود @@unique و@@index([schoolId]) لكل نموذج تجاري.

نقاط الدخول

نقطة الدخولالنطاقمسار التطبيقالتوثّقالغرض
SaaS Marketinged.databayt.org/(saas-marketing)عامlanding، التسعير، docs
SaaS Dashboarded.databayt.org/(saas-dashboard)DEVERLOPER فقطإدارة المنصّة
School Marketing{school}.databayt.org/s/[subdomain]/(school-marketing)عامصفحات المدرسة العامة
School Dashboard{school}.databayt.org/s/[subdomain]/(school-dashboard)موثَّقإدارة المدرسة

Edge middleware

يعمل edge proxy على كل طلب غير ثابت ويبقى تحت حدّ حجم Edge بتجنّب الاستيرادات الثقيلة. ترتيب المعالجة: تجاوز المسارات الثابتة وAPI، اكتشاف اللغة (NEXT_LOCALE cookie ← Accept-Language ← الافتراضي ar)، اكتشاف النطاق الفرعي، تصنيف المسار، فكّ تشفير JWT خفيف لفحوصات RBAC المسبقة، ثم إعادة كتابة الـURL مع ضبط x-subdomain للمستهلكين اللاحقين.

فكّ تشفير دور JWT في edge يستخدم base64 فقط — التحقّق التشفيري الكامل يحدث في server actions عبر auth(). الـJWT موجود أصلاً في cookie آمن httpOnly؛ فكّ تشفير edge للتوجيه فقط.

اكتشاف النطاق الفرعي

let subdomain: string | null = null
 
if (host.endsWith(".databayt.org") && !host.startsWith("ed.")) {
  // production: school.databayt.org → "school"
  subdomain = host.split(".")[0]
} else if (host.includes("---") && host.endsWith(".vercel.app")) {
  // Vercel preview: tenant---branch.vercel.app → "tenant"
  subdomain = host.split("---")[0]
} else if (host.includes("localhost") && host.includes(".")) {
  // dev: subdomain.localhost:3000 → "subdomain"
  const parts = host.split(".")
  if (parts.length > 1 && parts[0] !== "www" && parts[0] !== "localhost") {
    subdomain = parts[0]
  }
}

ed.databayt.org هو النطاق الرئيسي، ليس مستأجراً. فحص !host.startsWith("ed.") يمنع التصنيف الخاطئ.

إعادة كتابة الـURL

المستخدمون يرون مسارات نظيفة؛ الخادم يعالج مسارات ذات نطاق المستأجر.

User sees:    school.databayt.org/dashboard
Server sees:  school.databayt.org/en/s/school/dashboard
File lives:   src/app/[lang]/s/[subdomain]/(school-dashboard)/dashboard/page.tsx
url.pathname = `/${locale}/s/${subdomain}${pathWithoutLocale}`
const response = NextResponse.rewrite(url)
response.headers.set("x-subdomain", subdomain)

/login و/join موجودان عالمياً في /[lang]/(auth)/*، خارج بنية النطاق الفرعي — لا يُعاد كتابتهما. الـmatcher يستثني /_next/، /api/، وأي مسار يحوي امتداد ملف.

سياق المستأجر

getTenantContext() هو المصدر الوحيد للحقيقة لعزل المستأجرين. كل مكوّن خادم وإجراء خادم يجب أن يستدعيه.

export async function getTenantContext(): Promise<TenantContext> {
  // 1. impersonation cookie (DEVELOPER debugging)
  const impersonatedSchoolId =
    cookieStore.get("impersonate_schoolId")?.value ?? null
 
  // 2. subdomain from middleware → resolve to schoolId
  let headerSchoolId: string | null = null
  const subdomain = hdrs.get("x-subdomain")
  if (subdomain) {
    headerSchoolId = await getSchoolIdFromSubdomain(subdomain)
  }
 
  // 3. session schoolId from JWT
  const schoolId =
    impersonatedSchoolId ?? headerSchoolId ?? session?.user?.schoolId ?? null
 
  return { schoolId, requestId: null, role, isPlatformAdmin }
}
type TenantContext = {
  schoolId: string | null
  requestId: string | null
  role: UserRole | null
  isPlatformAdmin: boolean
}

استعلامات النطاق الفرعي ← schoolId تستخدم cache من طبقتين: Upstash Redis (5 دقائق، مشترك بين النسخ) ← Map في الذاكرة (دقيقة واحدة، 100 إدخال كحدّ أقصى، احتياطي لكل نسخة). الاستعلامات الفاشلة لا تُخزَّن. Redis اختياري — بدونه، طبقة Map لا تزال تخدم الحركة مع cold starts تضرب قاعدة البيانات.

القيود: طبقة Map لكل نسخة، لذا cold starts قد تضرب Redis أو قاعدة البيانات. تغييرات النطاق الفرعي تنتشر خلال 5 دقائق عالمياً ودقيقة واحدة لكل نسخة.

عزل قاعدة البيانات

كل نموذج ضمن نطاق المستأجر يحمل schoolId بثلاث ضمانات:

  1. حقل schoolId على كل نموذج تجاري.
  2. قيود @@unique ضمن نطاق schoolId بحيث تكون التصادمات بين المستأجرين مستحيلة.
  3. @@index([schoolId]) لأداء الاستعلام.
model Student {
  id       String @id @default(cuid())
  schoolId String
 
  school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
 
  @@unique([schoolId, studentId])
  @@unique([schoolId, grNumber])
  @@index([schoolId])
}

أنماط الاستعلام

// correct — includes schoolId
await db.student.findMany({
  where: { schoolId, yearLevel: "10" },
})
 
// wrong — missing schoolId, breaks tenant isolation
await db.student.findMany({
  where: { yearLevel: "10" },
})

حقن schoolId يدوي حالياً. رموز Auth، عموميات الكتالوج، ومستويات الاشتراك على مستوى المنصّة تفتقر إلى schoolId عمداً — راجع database للقائمة الكاملة.

إدارة النطاقات

كل مدرسة لها حقل domain فريد على نموذج School — يُسنَد أثناء onboarding.

model School {
  id     String @id @default(cuid())
  domain String @unique
}

DnsService يتحقّق من النطاقات الفرعية: من 3 إلى 63 حرفاً، أحرف وأرقام وشرطات فقط (RFC 1035)، بلا شرطات متتالية، مع حظر الكلمات المحجوزة (www، api، admin، mail، dashboard، portal، dev، test، staging، demo، preview، إلخ). نموذج DomainRequest يدعم اعتماد النطاقات المخصّصة بمسار pending → approved → verified؛ تكاملات مزوّدي DNS (Cloudflare، Route 53، Vercel) تُتبَّع منفصلة. راجع onboarding لخطوة الإسناد.

التخزين المؤقّت

الطبقةالتقنيةTTLالنطاق
حلّ المستأجرUpstash Redis5 دقائقمشترك بين النسخ
حلّ المستأجرMap في الذاكرةدقيقةلكل نسخة (احتياطي)
استعلامات قاعدة البياناتلا يوجد—لا تخزين مؤقّت
الصورNext.js Image60 ثانية كحدّ أدنىCDN

cache من طبقتين: يُفحَص Redis أوّلاً، ثم Map؛ الإدخالات الناقصة تسقط إلى قاعدة البيانات. Map يحمل 100 إدخال كحدّ أقصى.

النشر

البيئةنمط النطاقالمثال
Production*.databayt.orgschool.databayt.org
Previewtenant---branch.vercel.appdemo---feature-x.vercel.app
Development*.localhost:3000demo.localhost:3000

Production يتطلّب wildcard DNS *.databayt.org يشير إلى Vercel، شهادة SSL تغطّي *.databayt.org، وNEXT_PUBLIC_ROOT_DOMAIN=databayt.org.

الاختبار

أمان تعدّد المستأجرين مغطّى باختبارات Playwright الذرّية تحت /tests. مجموعات story تختبر: الوصول العام لـSaaS marketing، واجهة تسجيل الدخول، إعادة توجيه المسارات المحمية، قيود RBAC حسب الدور، عزل المستأجرين، تبديل اللغة، مسار المستخدم الجديد، SSO عبر النطاقات الفرعية. شغّل pnpm test:e2e:multi-tenant لتنفيذ المجموعة.

انظر أيضاً

  • Authentication — JWT، الجلسات، سلسلة أولويات إعادة التوجيه
  • OAuth — المزوّدون، adapter متعدّد المستأجرين، الحفاظ على callback، cookies
  • Authorization — فحوصات صلاحيات RBAC على مستوى الميزة
  • Database — نماذج Prisma، تحديد نطاق schoolId، النماذج بدون schoolId
  • Vercel platforms starter kit
  • NextAuth.js documentation
قاعدة البياناتالفهرس (Catalog)

On This Page

مسار الطلبنقاط الدخولEdge middlewareاكتشاف النطاق الفرعيإعادة كتابة الـURLسياق المستأجرعزل قاعدة البياناتأنماط الاستعلامإدارة النطاقاتالتخزين المؤقّتالنشرالاختبارانظر أيضاً

من تطوير Databayt ·

مرحباً بك في بالقلم.

رحلة عظيمة على وشك أن تبدأ.