Главная/Блог/Мультиарендная архитектура для SaaS на NestJS и Prisma: сотни клиентов — одно приложение, ноль утечек данных

Мультиарендная архитектура для SaaS на NestJS и Prisma: сотни клиентов — одно приложение, ноль утечек данных

Мультиарендная архитектура для SaaS на NestJS и Prisma: сотни клиентов — одно приложение, ноль утечек данных
Мультиарендность (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 — контекст арендатора, который доступен в любом слое приложения. Его жизненный цикл на одном запросе выглядит так:
  1. Идентификация. Арендатор определяется по поддомену (client.platform.ru), кастомному домену или claim tenant_id внутри JWT.
  2. Валидация. Middleware проверяет: арендатор существует, активен, подписка не истекла, домен не заблокирован.
  3. Сохранение контекста. Данные об арендаторе кладутся в AsyncLocalStorage — это быстрее, чем request-scoped провайдеры NestJS, и контекст доступен в сервисах без проброса параметров через все слои.
  4. Автофильтрация. Prisma-расширение читает контекст и само добавляет tenant_id во все запросы к базе.
  5. Фоновые задачи. Для очередей BullMQ tenant_id кладётся в payload задачи, и воркер восстанавливает контекст так же, как если бы это был HTTP-запрос.
Ключевой принцип: контекст арендатора — не глобальная переменная, а часть request-scoped состояния, иначе под нагрузкой начнутся «перепутанные» запросы.

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.
RLS добавляет немного накладных расходов, но превращает потенциальный инцидент с утечкой данных всех клиентов в единичную ошибку одного запроса. Для SaaS, где данные клиентов — главный актив, это оправданная страховка.

Эксплуатация: миграции, индексы и «шумные соседи»

Мультиарендность радикально удешевляет эксплуатацию: миграция Prisma Migrate выполняется один раз на всех клиентов, бэкап — один, мониторинг — один. Но появляются свои особенности. Во-первых, каждый индекс в общих таблицах должен начинаться с tenant_id: составные индексы вида (tenant_id, status, created_at) превращают поиск в десятках миллионов строк из full scan в мгновенный lookup. Во-вторых, connection pooling: сотни арендаторов генерируют тысячи соединений, поэтому между приложением и PostgreSQL ставим PgBouncer в transaction mode. В-третьих, «шумные соседи»: один клиент с импортом 100 000 строк не должен тормозить остальных — решается пер-арендаторскими rate limit, приоритетами в очередях и ограничением конкурентности воркеров на один tenant_id. Метрики тоже считаем в разрезе арендаторов: потребление CPU, размер данных, количество запросов — это основа тарификации и раннего обнаружения проблем.

Когда отдельная база всё-таки нужна

Есть сценарии, где общая база не подходит: крупный корпоративный клиент требует выделенную инфраструктуру в договоре SLA, регуляторика (например, требования к персональным данным по 152-ФЗ) предписывает хранение на отдельных ресурсах, или объём данных одного арендатора настолько велик, что ломает производительность общего кластера. Практичное решение — гибридная модель: кодовая база одна, а таблица маршрутизации указывает, к какой базе подключаться для каждого арендатора. Prisma позволяет работать с несколькими datasource, и тенант из общего пула просто «переезжает» на выделенную базу без переписывания логики. Чтобы гибрид не превратился в боль, слой определения арендатора нужно проектировать абстрактным с первого дня — тогда добавление выделенных баз станет конфигурацией, а не рефакторингом.

Итог: изоляция должна быть свойством системы, а не памятью разработчика

Мультиарендная архитектура — это всегда баланс между степенью изоляции и стоимостью эксплуатации. Для большинства SaaS оптимален общий кластер PostgreSQL с tenant_id в каждой таблице, автоматической фильтрацией на уровне Prisma extensions и страховкой в виде Row Level Security. Главный принцип: данные клиентов защищает архитектура, а не внимательность конкретного — тогда рост с 10 до 1000 арендаторов не потребует переписывания ядра платформы. Если проектируете SaaS на NestJS и Prisma, закладывайте мультиарендность с первого дня: достроить изоляцию в живом продукте с реальными данными клиентов значительно дороже и рискованнее, чем построить её сразу правильно.

Нужна разработка?

Оставьте заявку — обсудим ваш проект и предложим решение