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

Мы регулярно сталкиваемся с задачей построения мультитенантной SaaS-архитектуры, где каждый клиент работает в своём субдомене. Клиенту нужно, чтобы данные были изолированы, а адресная строка показывала его бренд. При этом база данных общая, чтобы упростить администрирование. Мы разрабатываем решение

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

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

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

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1418
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    983
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1242
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    997

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

Как настроить wildcard DNS и SSL?

Для обработки неограниченного числа клиентов без ручного добавления каждой записи используем wildcard DNS. Запись *.app.com направляет любой субдомен на IP сервера. Wildcard SSL-сертификат от Let's Encrypt автоматически покрывает app.com и все субдомены, что снижает затраты на инфраструктуру примерно на 60% по сравнению с покупкой отдельных сертификатов.

# 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 в год Бесплатно (Let's Encrypt) Бесплатно (один домен)
Сложность миграции Средняя (нужен редирект) Высокая (меняются URL)

Почему стоит выбирать субдомены, а не кастомные домены?

Кастомные домены дают полный брендинг, но требуют отдельного SSL для каждого клиента. Если у вас 500 клиентов — нужно 500 сертификатов. С 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 рабочих дней в зависимости от сложности приложения и количества моделей. Стоимость рассчитывается индивидуально на основе объёма работ. Получите консультацию — оценим ваш проект бесплатно. Мы гарантируем изоляцию данных и помогаем с интеграцией в существующий код.

Типичные ошибки и как их избежать

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

Если вы задумались о внедрении мультитенантности, свяжитесь с нами — обсудим ваш проект и подберём оптимальную архитектуру.