Мультитенантная CRM обслуживает нескольких клиентов на одной кодовой базе, и главный риск здесь один: данные одного клиента не должны попасть к другому. Мы закладываем изоляцию на уровне базы данных через Row-Level Security в Postgres, а не только в коде приложения, и проверяем её тестами на настоящем Postgres.
Что такое тенант и зачем изоляция на уровне базы
Тенантом называют одного клиента платформы: клуб, сеть студий или другая компания со своими филиалами, сотрудниками и клиентами. Все тенанты живут в одной схеме таблиц, а каждая строка помечена идентификатором тенанта.
Если фильтр «показывать только строки моего тенанта» стоит только в коде приложения, любая забытая проверка превращается в утечку. Row-Level Security переносит это правило в саму базу. Postgres сам отсекает чужие строки, даже если запрос написан без фильтра.
Как это устроено у нас
Схема опирается на четыре решения.
- У каждой таблицы есть колонка тенанта, и для неё включена политика изоляции.
- Связи между таблицами проверяются составным внешним ключом с тенантом. Родитель и потомок не могут принадлежать разным клиентам, и это гарантирует база, а не код.
- Филиал хранится отдельной таблицей, а не свободным текстом. Так не возникает расхождений вроде «Нарвская» и «нарвская».
- Представитель спортсмена вынесен в отдельную таблицу. Это готовит почву для семейных аккаунтов, где один родитель ведёт нескольких детей.
Как мы проверяли изоляцию
Проверка запускается на настоящем Postgres, поднятом внутри теста, а не на имитации. Тесты проходят по нескольким сценариям:
| Сценарий | Что ожидаем |
|---|---|
| Запрос без выбранного тенанта | Пусто по всем таблицам |
| Переключение между тенантами | Видны только строки текущего тенанта |
| Вставка строки чужого тенанта | Отклоняется базой |
| Изменение чужой строки | Отклоняется базой |
| Удаление родителя | Каскад работает внутри тенанта |
Что нашли по дороге
Первая версия теста проходила по неверной причине. RLS молча не действует для суперпользователя и владельца таблицы, а тест шёл именно от суперпользователя. Пришлось завести отдельную роль приложения и прогонять проверки от неё.
Вторая находка касается пула соединений. Идентификатор тенанта нужно задавать на время транзакции, иначе он «прилипает» к переиспользуемому соединению и следующий запрос увидит данные предыдущего клиента.
Частые вопросы
Достаточно ли RLS для защиты персональных данных? Нет, это один слой из нескольких. Дополнительно нужны роли доступа, журнал действий и решения по месту хранения данных. Требования 152-ФЗ мы прорабатываем отдельно, а не считаем изоляцию строк полным ответом.
Чем это лучше отдельной базы на каждого клиента? Проще обновлять и поддерживать одну кодовую базу. Отдельная база даёт более жёсткую границу и иногда нужна по требованиям заказчика. Выбор зависит от задачи и бюджета.
Можно ли переехать с одного клиента на несколько? Можно, но тенантность лучше закладывать с самого начала, потому что добавлять её в готовую систему сложнее.
Если вам нужна CRM для нескольких клиентов или филиалов, опишите задачу в форме.