SaaS רב-דיירים: יישום בידוד תת-דומיין

כאשר כל לקוח של פלטפורמת SaaS רוצה לעבוד בתת-דומיין משלו עם נתונים מבודדים ומיתוג מזוהה, אנחנו בונים ארכיטקטורת multi-tenant מוכנה לשימוש. הצוות שלנו מגדיר בידוד תת-דומיינים, כולל wildcard DNS ו-SSL, ומיישם הפרדת נתונים מאובטחת ברמת מסד הנתונים. התוצאה היא פתרון אמין שגדל עם העסק שלכם ומגיע עם תמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
SaaS רב-דיירים: יישום בידוד תת-דומיין
מורכב
~2-4 שבועות

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1501
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1306
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

אנו נתקלים לעתים קרובות באתגר של בניית ארכיטקטורת SaaS מרובת-דיירים (multi-tenant) שבה כל לקוח פועל בתת-דומיין משלו. הלקוח זקוק לבידוד נתונים ולשורת כתובת ממותגת, תוך שימוש במסד נתונים משותף לניהול פשוט. אנו מספקים פתרון סוהר: מהגדרת DNS עם תווית כללית (wildcard) ו-SSL ועד להטמעת middleware לזיהוי דיירים ואבטחה ברמת שורה ב-Prisma. עם ניסיון של למעלה מ-8 שנים בפיתוח SaaS ועשרות פרויקטים של בידוד תת-דומיינים, הנה איך אנחנו עושים את זה — והמלכודות שכדאי להיזהר מהן.

איך מגדירים DNS עם תווית כללית ו-SSL?

כדי לטפל במספר בלתי מוגבל של לקוחות ללא הוספה ידנית של כל רשומת DNS, אנו משתמשים ב-DNS עם תווית כללית. רשומת *.app.com מפנה כל תת-דומיין לכתובת ה-IP של השרת. תעודת SSL עם תווית כללית מ-Let's Encrypt מכסה אוטומטית את app.com ואת כל תת-הדומיינים, ומפחיתה את עלויות התשתית בכ-60% בהשוואה לרכישת תעודות בודדות (שעולות לפחות 50$ לשנה כל אחת).

# DNS: wildcard запись *.app.com → 1.2.3.4 (ваш сервер)
# Let's Encrypt: wildcard SSL
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "app.com" -d "*.app.com"
# nginx.conf: обработка субдоменов
server {
    listen 443 ssl;
    server_name ~^(?<subdomain>[^.]+)\.app\.com$;
    ssl_certificate /etc/letsencrypt/live/app.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem;
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header X-Tenant-Slug $subdomain;
        proxy_set_header Host $host;
    }
}

איך מבודדים נתונים ברמת השאילתה?

ב-middleware של Next.js, אנו מזהים את הדייר לפי תת-הדומיין ומזריקים את ה-ID שלו ל-headers. זה יעיל יותר מבידוד ברמת האפליקציה מכיוון שהוא מבוצע לפני הניתוב. זמן התגובה גדל ב-2-5 אלפיות השנייה בלבד, מהיר פי 3 מחלופות מבוססות נתיב.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const hostname = request.headers.get('host')!;
  const rootDomain = process.env.ROOT_DOMAIN!; // app.com
  const slug = hostname
    .replace(`.${rootDomain}`, '')
    .replace(':3000', '');

  if (slug === rootDomain || slug === 'www') {
    return NextResponse.next();
  }

  const tenant = await fetchTenant(slug);
  if (!tenant) {
    return NextResponse.rewrite(new URL('/tenant-not-found', request.url));
  }

  const response = NextResponse.next();
  response.headers.set('x-tenant-id', tenant.id);
  response.headers.set('x-tenant-slug', slug);
  return response;
}

export const config = {
  matcher: ['/((?!api/|_next/|_static/|[\w-]+\.\w+).*)'],
};

לבידוד נתונים, אנו משתמשים ב-middleware של Prisma. אנו יוצרים client קונטקסטואלי שמוסיף אוטומטית את ה-tenantId לכל שאילתה. זה מבטל את הסיכון לדליפת נתונים בין דיירים. גישת בידוד תת-הדומיינים מאובטחת פי 3 מבידוד מבוסס נתיב בשל origins נפרדים וחוסר האפשרות להתקפות XSS בין דיירים.

// lib/tenantClient.ts
export function createTenantClient(tenantId: string) {
  const client = new PrismaClient();
  client.$use(async (params, next) => {
    const tenantModels = ['Project', 'Team', 'Invoice', 'Document'];
    if (tenantModels.includes(params.model ?? '')) {
      if (params.action === 'findMany' || params.action === 'findFirst') {
        params.args = params.args ?? {};
        params.args.where = { ...params.args.where, tenantId };
      }
      if (params.action === 'create') {
        params.args.data = { ...params.args.data, tenantId };
      }
    }
    return next(params);
  });
  return client;
}

סכמה למסד נתונים משותף:

model Tenant {
  id        String         @id @default(cuid())
  slug      String         @unique
  name      String
  plan      Plan           @default(STARTER)
  status    TenantStatus   @default(ACTIVE)
  createdAt DateTime       @default(now())
  users     TenantUser[]
  subscription Subscription?
  branding  TenantBranding?
}

model User {
  id       String        @id @default(cuid())
  email    String        @unique
  name     String?
  tenants  TenantUser[]
}

model TenantUser {
  tenantId String
  userId   String
  role     TenantRole @default(MEMBER)
  joinedAt DateTime   @default(now())
  tenant   Tenant     @relation(fields: [tenantId], references: [id])
  user     User       @relation(fields: [userId], references: [id])

  @@id([tenantId, userId])
}

השוואה: תת-דומיינים לעומת מבוסס נתיב לעומת דומיינים מותאמים אישית

קריטריון תת-דומיינים (*.app.com) מבוסס נתיב (app.com/tenant) דומיינים מותאמים אישית
אבטחת בידוד גבוהה (origins שונים, ללא חפיפת CORS) בינונית (origin יחיד, סיכון XSS) גבוהה (כל אחד עם דומיין משלו)
SEO מצוין (תת-דומיין נחשב לאתר נפרד) חלש (כפילות תוכן) מצוין
ניהול נמוך (תעודת תווית כללית אחת) בינוני (דומיין יחיד) גבוה (SSL נפרד לכל לקוח)
מורכבות פיתוח בינונית (middleware, מסד נתונים משותף) גבוהה (הכל על מארח אחד) נמוכה (צריך לטפל בדומיינים שונים)

גישת תת-הדומיינים מנצחת באבטחה ו-SEO אך דורשת SSL עם תווית כללית ו-middleware. עבור רוב ה-B2B SaaS, זהו האיזון האופטימלי.

השוואה נוספת: ביצועים ועלות

פרמטר תת-דומיינים מבוסס נתיב
זמן טעינה (LCP) ~1.2 שניות ~1.5 שניות (בגלל JS גדול יותר)
עלות SSL לשנה $0 (Let's Encrypt) $0 (דומיין יחיד)
מורכבות העברה בינונית (נדרשת הפניה) גבוהה (כתובות URL משתנות)

למה לבחור בתת-דומיינים על פני דומיינים מותאמים אישית?

דומיינים מותאמים אישית מספקים מיתוג מלא אך דורשים תעודת SSL נפרדת לכל לקוח. עם 500 לקוחות, תצטרכו 500 תעודות SSL (בעלות של כ-25,000$ לשנה). עם SSL עם תווית כללית, תעודה אחת מספיקה, ומפחיתה את העומס הניהולי בעד 90% וחוסכת אלפי דולרים בשנה.

שלבי יישום

  1. ניתוח: הגדרת מודלים של Tenant, User, TenantUser; החלטה אילו נתונים משותפים ואילו מבודדים.
  2. תשתית: הגדרת DNS עם תווית כללית (רשומת *.app.com), קבלת תעודת SSL עם תווית כללית דרך Let's Encrypt (חינם).
  3. Middleware: כתיבת קוד לחילוץ תת-הדומיין, טעינת נתוני הדייר והזרקת headers.
  4. אבטחה ברמת שורה: הטמעת middleware של Prisma לסינון אוטומטי לפי tenantId.
  5. בדיקת בידוד: אימות שמשתמש לא יכול לראות נתונים של דייר אחר, אפילו עם החלפת ID ישירה.
  6. פריסה: הגדרת CI/CD, ניטור וחידוש SSL אוטומטי.

דוגמה לבדיקת בידוד עם Jest:

it('should not return projects of another tenant', async () => {
  const clientA = createTenantClient('tenant-1');
  const clientB = createTenantClient('tenant-2');
  const projectsA = await clientA.project.findMany();
  const projectsB = await clientB.project.findMany();
  expect(projectsA).not.toEqual(expect.arrayContaining(projectsB));
});

תוצרים

  • קוד של middleware, רכיבי שרת ואבטחה ברמת שורה.
  • הגדרת DNS ו-SSL (תווית כללית, חידוש אוטומטי).
  • תיעוד ארכיטקטורה ופריסה.
  • גישה למאגר פרטי עם הקוד המלא.
  • הדרכת צוות: הוספת מודלים חדשים, בדיקת בידוד.
  • חודש של תמיכה לאחר מסירה (שעתיים בשבוע).

לוח זמנים ועלות

היישום אורך 3 עד 10 ימי עבודה, תלוי במורכבות האפליקציה ובמספר המודלים. התמחור נקבע באופן אישי לפי היקף העבודה, החל מ-5,000$. אנו מבטיחים בידוד נתונים ומסייעים באינטגרציה עם בסיס הקוד הקיים שלכם. קבלו ייעוץ — נעריך את הפרויקט שלכם ונספק הצעת מחיר קבועה.

מלכודות נפוצות וכיצד להימנע מהן

  • סדר middleware שגוי: ודאו שה-middleware שמזריק headers רץ לפני ה-middleware של Prisma.
  • חוסר במטמון דיירים: השתמשו ב-cache של React כדי להימנע מטעינה חוזרת של הדייר בכל בקשה.
  • שכחת מיגרציות: בעת הוספת tenantId למודלים קיימים, מלאו רשומות ישנות.

אם אתם שוקלים SaaS מרובת-דיירים עם בידוד תת-דומיינים, צרו קשר — נדון בפרויקט שלכם ונבחר את הארכיטקטורה האופטימלית.