مشكلة التفكير في عميل واحد
عند البدء ببناء منتج برمجيات كخدمة، قد يبدو تصميم قاعدة البيانات والواجهات حول عميل واحد خيارًا مغريًا. كل شيء أبسط: مخطط واحد وجداول واحدة ونشر واحد. لكن عند انضمام عميل ثانٍ يحتاج إلى عزل بياناته، ستضطر إلى إدخال تعدد المستأجرين في بنية لم تُصمم له.
تتناول هذه المقالة القرارات المعمارية التي اتخذتها أثناء بناء خادم نقاط بيع متعدد المستأجرين يخدم مواقع تجزئة تابعة لمؤسسات متعددة.
النهج: عزل المستأجرين على مستوى الصف
هناك ثلاثة أنماط شائعة:
- قواعد بيانات منفصلة — عزل فعلي بتكلفة تشغيل مرتفعة.
- مخططات منفصلة — عزل جيد مع تعقيد في ترحيلات قاعدة البيانات.
- مخطط مشترك مع معرّف المستأجر — بداية أسهل تتطلب التزامًا صارمًا بتصفية الاستعلامات.
يوفر العزل على مستوى الصف لمعظم منتجات SaaS المبكرة توازنًا مناسبًا بين البساطة والعزل.
// Every table that holds tenant data includes a tenantId
model Product {
id String @id @default(cuid())
tenantId String
name String
price Decimal
tenant Tenant @relation(fields: [tenantId], references: [id])
@@index([tenantId])
}
فرض عزل المستأجرين
يكمن الخطر في تسرب البيانات عرضًا إذا نسي استعلام تصفية tenantId. عالجت ذلك بنمط المستودع:
// src/lib/db/repositories/product.repository.ts
export function createProductRepository(tenantId: string) {
return {
findAll: () =>
prisma.product.findMany({
where: { tenantId },
orderBy: { createdAt: "desc" },
}),
findById: (id: string) =>
prisma.product.findFirst({
where: { id, tenantId }, // tenantId always included
}),
};
}
تمر كل عملية على قاعدة البيانات عبر المستودع الذي يتلقى سياق المستأجر من الجلسة دائمًا.
تحديد المستأجر في Next.js App Router
باستخدام App Router، أستخرج المستأجر من الجلسة في التخطيط الجذري وأمرره عبر سياق React:
// app/[tenantSlug]/layout.tsx
export default async function TenantLayout({ params, children }) {
const tenant = await getTenantBySlug(params.tenantSlug);
if (!tenant) notFound();
return (
<TenantProvider tenant={tenant}>
{children}
</TenantProvider>
);
}
تقرأ Server Actions المستأجر من الجلسة بدل مدخلات المستخدم، للحماية من هجمات الوصول المباشر غير المصرح به إلى الكائنات (IDOR).
الدروس المستفادة
- لا تثق بمعرّف المستأجر القادم من العميل. استمد السياق دائمًا من الجلسة الموثقة.
- أضف فهارس لأعمدة
tenantId. تتراجع خطط الاستعلام سريعًا دونها. - خطط لترحيل البيانات مبكرًا. تحتاج إضافة
tenantIdللجداول القائمة دون توقف إلى تعبئة مدروسة للبيانات القديمة. - استخدم الحذف المنطقي بدل النهائي. تتطلب بيانات المستأجر تدقيقًا، وتكون أختام
deletedAtالزمنية أكثر أمانًا.
يكافئ تعدد المستأجرين الاستثمار المبكر في التصميم. عندما تستقر الأنماط من البداية، يصبح التوسع إلى آلاف المستأجرين تحديًا في قواعد البيانات والبنية التحتية بدل إعادة كتابة البرمجيات.