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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

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

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

Этапы разработки

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

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

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

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

Разработка SaaS-платформ

Мы знаем эту боль наизусть. Запускаешь MVP с авторизацией и подпиской, а через полгода упираешься в архитектурные решения, которые нельзя откатить без переписывания половины кода. Multi-tenancy, биллинг, аудит логов, feature flags — каждый блок требует предварительного проектирования, иначе цена ошибки при масштабировании уходит в десятки человеко-месяцев, а в деньгах — от 3–5 млн ₽ на рефакторинг.

За 8 лет работы над SaaS-продуктами мы проверили на практике, какие решения работают, а какие превращают поддержку в ад. Ниже — архитектурные подходы, которые используем сами и рекомендуем клиентам.

Как мы строим multi-tenancy: изоляция без оверхеда

Первое, что решаем — схема разделения данных. Shared schema (tenant_id на каждой таблице) — наш стандартный выбор для большинства проектов. Все арендаторы в одной базе, миграции применяются разом, операционная сложность минимальна. В Laravel реализуем через Global Scope:

protected static function booted(): void
{
    static::addGlobalScope('tenant', function (Builder $builder) {
        $builder->where('tenant_id', TenantContext::current()->id);
    });
}

Глобальный скоуп — только первый уровень защиты. Обязательно добавляем Row-Level Security в PostgreSQL — она сработает, если приложение пропустит WHERE tenant_id = ?:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::uuid);

Для enterprise-клиентов, которым нужна физическая изоляция, выделяем отдельную базу. Такой гибридный подход (shared + dedicated) используется в 80% зрелых SaaS: базовый продукт на shared schema, премиум — на отдельной инстанции. Мы внедряем его с первого спринта, чтобы не переписывать логику позже.

Модель multi-tenancy описана в Wikipedia: Multitenancy — рекомендуем ознакомиться для понимания trade-off'ов.

Почему биллинг — самый недооценённый блок

Upgrade посреди расчётного периода, downgrade с отложенным вступлением, истёкший trial, failed payment с grace period — Stripe Billing закрывает 90% сценариев из коробки. Обязательно обрабатываем вебхуки (customer.subscription.updated, invoice.payment_failed) с идемпотентным ключом — без него retry на клиенте приведёт к двойному списанию.

Для рынка СНГ — ЮKassa или Tinkoff recurring. API менее удобны, но покрывают требования 54-ФЗ.

Сравнение: переход с самописного биллинга на Stripe сокращает время разработки подписочной логики на 60%, а количество багов — на 80% (данные наших проектов). Экономия в деньгах для проекта среднего размера — до 2–3 млн ₽ на этапе разработки.

Onboarding: как не потерять пользователя до aha-moment

Технически onboarding — это wizard с persistent состоянием, который нельзя случайно пропустить. Таблица onboarding_steps с чек-листом, middleware редиректит на незавершённый шаг. После завершения — флаг в user settings, middleware отключается.

Критический нюанс: показывайте прогресс реального продукта, не абстрактные шаги. «Создайте первый отчёт» вместо «Завершите шаг 3 из 5». Мы используем drip-кампании через Customer.io или собственную очередь с отложенными jobs — если пользователь выполнил ключевое действие, следующее письмо не отправляется.

Feature flags и управление доступом

SaaS с тарифами требует гранулярного контроля. Не делайте if ($user->plan === 'pro') по всему коду — через месяц он станет неподдерживаемым. Вместо этого:

  • Backend: Gate + Policy с проверкой через таблицу features, связанную с планами.
  • Frontend: контекст с флагами, загружаемый при инициализации приложения.
  • Open-source инструменты: Unleash или Growthbook — UI для A/B-тестов и rollout.

Как защитить API от агрессивных клиентов

Rate limiting — must-have для публичного API. Один клиент может положить всех остальных. В Laravel используем Redis с sliding window counter:

Тариф Лимит Заголовки в ответе
Free 100 req/h X-RateLimit-Limit: 100
Pro 1 000 req/h X-RateLimit-Limit: 1000
Enterprise 10 000 req/h X-RateLimit-Limit: 10000

Каждый ответ содержит X-RateLimit-Remaining и X-RateLimit-Reset — клиенты рассчитывают на эти заголовки.

Аудит-логи и мониторинг: что, кто и когда

Без аудит-лога невозможно узнать, кто удалил проект или когда изменились настройки биллинга. Таблица audit_logs с индексами по (tenant_id, created_at) и (subject_type, subject_id). В Laravel — Observer'ы на ключевых моделях.

Пример реализации Observer для Model
class OrderObserver
{
    public function created(Order $order): void
    {
        AuditLog::create([
            'tenant_id' => $order->tenant_id,
            'user_id' => auth()->id(),
            'action' => 'created',
            'subject_type' => Order::class,
            'subject_id' => $order->id,
        ]);
    }
}

Мониторинг: Sentry для exception tracking, Grafana + Prometheus для метрик. Алерты на error rate > 5% и response time p95 > 2s.

Опыт нашей команды и гарантии

Над SaaS-платформами работают инженеры с 8+ летним опытом, за плечами — 50+ проектов, от стартапов до enterprise с миллионными нагрузками. Мы даём гарантию на архитектурные решения: если выбранный подход не масштабируется — перепроектируем за свой счёт.

Deliverables и гарантии

  • Документация архитектуры: схемы, ERD, sequence diagrams.
  • Настройка CI/CD (GitHub Actions / GitLab CI).
  • Доступы к репозиторию, стейджингу и продакшену.
  • Обучение команды: 2–3 сессии по код-ревью и runbook.
  • Post-launch поддержка 1 месяц.
  • Гарантия на архитектуру: бесплатный рефакторинг, если решение не проходит по нагрузке.

Процесс работы

  1. Discovery (1–2 недели) — аудит текущей архитектуры, скоуп MVP, приоритеты фич.
  2. Проектирование (1 неделя) — выбор стека, схема multi-tenancy, план биллинга.
  3. Разработка (4–12 недель) — спринты по 2 недели, демо после каждого.
  4. Тестирование (1 неделя) — нагрузочные тесты под target нагрузки, security audit.
  5. Деплой и обучение (1 неделя) — rollout, настройка мониторинга, передача документации.

Ориентиры по срокам

Этап Срок
MVP (core features + auth + billing) 12–16 недель
Полноценный продукт с admin panel 20–28 недель
Enterprise SaaS с multi-tenancy + audit 28–40 недель

Стоимость рассчитывается индивидуально — свяжитесь с нами, мы оценим проект за 2 дня. Закажите разработку под ключ: от проектирования до деплоя с гарантией архитектуры. Получите консультацию по архитектуре вашего продукта — первый час бесплатно.