Мультиарендность (multi-tenancy) — это архитектурный подход, при котором одно приложение и одна инфраструктура обслуживают десятки и сотни клиентов, но каждый видит только свои данные. Для SaaS это не техническая прихоть, а вопрос экономики: именно мультиарендность позволяет снижать стоимость обслуживания одного клиента по мере роста платформы. В этой статье разбираем, какие модели изоляции данных существуют, почему мы чаще всего выбираем общую базу с полем tenant_id и как NestJS вместе с Prisma защищают платформу от утечки данных между арендаторами. Материал будет полезен основателям SaaS-сервисов и CTO, которые планируют масштабирование.
Зачем SaaS мультиарендность: экономика и скорость релизов
Подход «один клиент — отдельная копия приложения» упирается в потолок уже на десятках клиентов. Если у вас 40 установок, каждый релиз превращается в многочасовую операцию: 40 деплоев, 40 миграций базы, 40 точек отказа и мониторинг каждого инстанса отдельно. В мультиарендной платформе один деплой обновляет продукт сразу для всех клиентов, а багфикс не требует согласования окна обслуживания с каждым арендатором. Экономика тоже очевидна: инфраструктурные затраты на одного клиента падают в разы, а подключение нового аккаунта занимает минуты — зарегистрировали компанию, выдали поддомен, ограничили функционал тарифом. Единственная цена, которую приходится платить, — строгая дисциплина изоляции данных, и её нужно строить системно, а не «на честном слове» разработчиков.Три модели изоляции данных
В индустрии сложилось три основных подхода, и выбор между ними определяет всю дальнейшую эксплуатацию платформы:- Отдельная база данных на клиента — максимальная изоляция и простое удаление данных клиента («право на забвение» решается дропом базы). Но операции дорожают линейно: 200 клиентов — это 200 наборов миграций, бэкапов и соединений. Подходит для enterprise-сегмента, но убивает экономику массового SaaS.
- Отдельная схема (schema) в общей базе — компромисс: данные логически разделены, инстанс один. На практике быстро упирается в ограничения: сотни схем усложняют миграции Prisma, мониторинг и пул соединений, а выигрываете вы немного по сравнению с общими таблицами.
- Общая база с колонкой tenant_id в каждой таблице — самый распространённый вариант для B2B SaaS: один кластер PostgreSQL, один набор миграций, фильтрация на уровне запросов. Требует автоматизации, зато масштабируется до сотен и тысяч арендаторов.
Почему мы чаще выбираем общую базу с tenant_id
Для 90% SaaS-платформ, где объём данных клиента измеряется мегабайтами и единицами гигабайтов, общий кластер PostgreSQL спокойно выдерживает сотни арендаторов. Когда данных становится больше, конструкция масштабируется эволюционно: партиционирование таблиц по tenant_id, реплики для чтения, и лишь в крайнем случае — шардирование. Главный риск этой модели очевиден: одна забытая WHERE-условие — и клиент А увидит данные клиента Б. Поэтому ставка делается не на дисциплину, а на архитектуру: фильтрация по арендатору должна происходить автоматически, на уровне фреймворка и ORM, чтобы разработчик физически не мог её пропустить. А страховкой от ошибок в обход кода служат механизмы самой базы данных.Как NestJS определяет арендатора на каждом запросе
Ядро мультиарендности на NestJS — контекст арендатора, который доступен в любом слое приложения. Его жизненный цикл на одном запросе выглядит так:- Идентификация. Арендатор определяется по поддомену (client.platform.ru), кастомному домену или claim tenant_id внутри JWT.
- Валидация. Middleware проверяет: арендатор существует, активен, подписка не истекла, домен не заблокирован.
- Сохранение контекста. Данные об арендаторе кладутся в AsyncLocalStorage — это быстрее, чем request-scoped провайдеры NestJS, и контекст доступен в сервисах без проброса параметров через все слои.
- Автофильтрация. Prisma-расширение читает контекст и само добавляет tenant_id во все запросы к базе.
- Фоновые задачи. Для очередей BullMQ tenant_id кладётся в payload задачи, и воркер восстанавливает контекст так же, как если бы это был HTTP-запрос.
Prisma: автоматическая фильтрация вместо надежды на внимательность
В Prisma изоляция строится через client extensions: query-компонент перехватывает каждый запрос и добавляет условие по tenant_id в where для чтения и в data для записи. Разработчик пишет обычный prisma.user.findMany() — а получает запрос вида WHERE tenant_id = '...' AND ... . Важно, что уникальные ключи тоже составные: уникальность email соблюдается внутри арендатора, а не во всей таблице — иначе два клиента не смогут завести пользователей с одинаковым логином. Сырые запросы ($queryRaw) либо запрещены код-ревью и линтером, либо обёрнуты в обязательный механизм передачи tenant_id. Завершающий слой — интеграционные тесты: создаём двух арендаторов, заполняем данными и проверяем, что API первого не возвращает ни одной записи второго. Такой тест гоняется на каждый merge и ловит утечки до продакшена.Второй рубеж обороны: Row Level Security в PostgreSQL
Автоматизация в коде — главный, но не единственный рубеж. Постройка «эшелонированной обороны» выглядит так:- Слой 1 — фильтрация в Prisma. Автоматическое добавление tenant_id через extensions покрывает 99% запросов приложения.
- Слой 2 — Row Level Security. Для каждой таблицы включается политика вида USING (tenant_id = current_setting('app.tenant_id')::uuid). Перед запросом приложение в транзакции выполняет SET LOCAL app.tenant_id = '...', и даже если SQL построен без фильтра — база молча отрежет чужие строки.
- Слой 3 — аудит и алерты. Логируются запросы без фильтра по арендатору, попытки доступа внерабочим временем и аномальные объёмы выгрузок по одному tenant_id.

