Мультитенантний SaaS: реалізація через піддомени

Ми регулярно стикаємося з задачею побудови мультитенантної SaaS-архітектури, де кожен клієнт працює в своєму субдомені. Клієнту потрібно, щоб дані були ізольовані, а адресний рядок показував його бренд. При цьому база даних спільна, щоб спростити адміністрування. Ми розробляємо рішення під ключ: від

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Мультитенантний SaaS: реалізація через піддомени
Складний
~2-4 тижні

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1422
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1288
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    984
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1250
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    988
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    1001

Ми регулярно стикаємося з задачею побудови мультитенантної SaaS-архітектури, де кожен клієнт працює в своєму субдомені. Клієнту потрібно, щоб дані були ізольовані, а адресний рядок показував його бренд. При цьому база даних спільна, щоб спростити адміністрування. Ми розробляємо рішення під ключ: від налаштування wildcard DNS і SSL до реалізації middleware для визначення тенанта та row-level security в Prisma. Наш досвід — понад 8 років у розробці SaaS, десятки проєктів з піддоменною ізоляцією. У цій статті розбираємо, як ми це робимо, і які підводні камені зустрічаються.

Мультитенантний SaaS: налаштування wildcard DNS і SSL

Для обробки необмеженої кількості клієнтів без ручного додавання кожного запису використовуємо wildcard DNS. Запис *.app.com направляє будь-який субдомен на IP сервера. Wildcard SSL-сертифікат від Let's Encrypt автоматично покриває app.com і всі субдомени, що знижує витрати на інфраструктуру приблизно на 60% порівняно з покупкою окремих сертифікатів. Економія для 500 клієнтів: $0 за сертифікат (Let's Encrypt безкоштовно) замість $50/рік за кожен кастомний домен — загалом $25 000 на рік.

# 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; } } 

Як ізолювати дані на рівні запитів?

У Next.js middleware визначаємо тенанта за субдоменом та інжектуємо його ID в заголовки. Це ефективніше, ніж ізоляція на рівні застосунку, оскільки виконується до маршрутизації. Час відгуку при цьому збільшується всього на 2-5 мс.

// 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+).*)'], }; 

Для ізоляції даних використовуємо Prisma middleware. Створюємо контекстний клієнт, який автоматично додає tenantId до кожного запиту. Це виключає ризик витоку даних між тенантами. Порівняно з path-based підходом, безпека ізоляції вища в 3 рази за рахунок розділення origin'ів та неможливості 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]) } 

Порівняння підходів: субдомени vs. path-based vs. кастомні домени

Критерій Субдомени (*.app.com) Path-based (app.com/tenant) Кастомні домени
Безпека ізоляції Висока (різні origin, CORS не перетинаються) Середня (один origin, ризик XSS) Висока (кожен свій домен)
SEO Відмінна (субдомен вважається окремим сайтом) Погана (дублі контенту) Відмінна
Адміністрування Низьке (один wildcard сертифікат) Середнє (один домен) Високе (потрібен окремий SSL для кожного)
Простота розробки Середня (middleware, однакова БД) Висока (все на одному хості) Низька (потрібна обробка різних доменів)

Субдоменний підхід виграє за безпекою та SEO, але вимагає налаштування wildcard SSL та middleware. Для більшості B2B SaaS це оптимальний баланс.

Додаткове порівняння: продуктивність і вартість

Параметр Субдомени Path-based
Час завантаження (LCP) ~1.2 с ~1.5 с (через більший JS)
Вартість SSL на рік $0 (Let's Encrypt) $50 (один домен)
Складність міграції Середня (потрібен редирект) Висока (змінюються URL)

Чому варто обирати субдомени, а не кастомні домени?

Кастомні домени дають повний брендинг, але вимагають окремого SSL для кожного клієнта. Якщо у вас 500 клієнтів — потрібно 500 сертифікатів, що коштує $25 000/рік. З wildcard SSL достатньо одного безкоштовного. Економія на адмініструванні сягає 90%.

Етапи реалізації мультитенантної архітектури

  1. Аналітика: визначаємо модель Tenant, User, TenantUser; вирішуємо, які дані спільні, які — ізольовані.
  2. Інфраструктура: налаштовуємо wildcard DNS (запис *.app.com), отримуємо wildcard SSL сертифікат через Let's Encrypt.
  3. Middleware: пишемо код для вилучення субдомена, завантаження тенанта, інжекції заголовків.
  4. Row-Level Security: реалізуємо Prisma middleware для автоматичної фільтрації за tenantId.
  5. Тестування ізоляції: перевіряємо, що користувач не бачить дані чужого тенанта, навіть при прямій підстановці ID.
  6. Деплой: налаштовуємо CI/CD, моніторинг, SSL renewal.

Приклад тесту ізоляції на 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, серверних компонентів та Row-Level Security.
  • Налаштування DNS і SSL (wildcard, автоматичне оновлення).
  • Документація з архітектури та розгортання.
  • Навчання команди: як додавати нові моделі, як тестувати ізоляцію.
  • Підтримка протягом 1 місяця після здачі.

Терміни та вартість

Термін реалізації — від 3 до 10 робочих днів залежно від складності застосунку та кількості моделей. Вартість розраховується індивідуально на основі обсягу робіт. Отримайте консультацію — оцінимо ваш проєкт безкоштовно. Ми гарантуємо ізоляцію даних і допомагаємо з інтеграцією в існуючий код.

Типові помилки та як їх уникнути

  • Неправильний порядок middleware: перевіряйте, що middleware із заголовків спрацьовує до Prisma middleware.
  • Відсутність кешування завантаження тенанта: використовуйте cache з React, щоб не завантажувати тенанта на кожен запит.
  • Забули про міграції: при додаванні tenantId в існуючі моделі потрібно заповнити його для старих записів.

Якщо ви задумалися про впровадження мультитенантності, зв'яжіться з нами — обговоримо ваш проєкт і підберемо оптимальну архітектуру.