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

Когда каждый клиент SaaS-платформы хочет работать в собственном субдомене с изолированными данными и узнаваемым брендом, мы разрабатываем мультитенантную архитектуру под ключ. Наша команда настраивает субдоменную изоляцию, включая wildcard DNS и SSL, а также реализует безопасное разделение данных на уровне базы. Результат — надёжное решение, которое масштабируется вместе с вашим бизнесом и сопровождается нами на всех этапах.

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

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

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

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

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

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

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

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1474
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1326
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1024
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1283
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1022
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1021

Мы регулярно сталкиваемся с задачей построения мультитенантной 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 в существующие модели, нужно заполнить его для старых записей.

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