П Прорвёмся!
← Все статьи
· 5 мин DjangoCRMMulti-tenancyArchitecture

Когда клиентов много, а база одна: что мы узнали, пока строили мультитенантную CRM

Номера телефонов лежат не в схеме клиента. Код подписи генерируется не там, где мы сначала сделали. И ещё несколько вещей, которые стали понятны только по ходу разработки.

Дмитрий Дмитрий лид-разработчик «Прорвёмся!»

Когда начинаешь мультитенантный продукт, кажется, что главное — правильно раздать client_id и не забыть фильтр в каждом запросе.

Потом оказывается, что телефонные номера нельзя хранить там же, где сделки. Что код электронной подписи не должен генерироваться там, где мы его сначала сгенерировали. И что входящий звонок приходит раньше, чем ты знаешь, кому звонить.

Это не баги. Это вещи, которые становятся очевидны, только когда архитектура уже работает и начинает показывать свои настоящие ограничения. В prvms.crm мы прошли через несколько таких открытий.

Телефонный номер — не данные клиента

Самое неочевидное следствие мультитенантной архитектуры касается телефонии.

У нас один SIP-транк на всех клиентов. Это осознанный выбор: мы не хотим заставлять каждого клиента настраивать свою АТС. Но из этого выбора вырастает архитектурное ограничение, которое сначала не видно.

Когда звонок приходит на платформу, мы ещё не знаем, какому клиенту он адресован. Запрос только поступил на общий номер, tenant не разрешён, схема не выбрана. Единственное, что у нас есть — это DID, номер, на который позвонили. По этому номеру мы должны найти клиента.

А теперь вспомним про изоляцию: данные клиента лежат в его схеме. Но чтобы попасть в схему, надо сначала узнать, чья это схема. А чтобы узнать — надо посмотреть на номер. А номер лежит… где?

Если номер положить в схему клиента, мы попадаем в тупик: чтобы найти клиента по номеру, надо войти в схему; чтобы войти в схему, надо знать клиента. Поэтому номера живут в общем пространстве, вне клиентских схем. Это не дизайн-решение. Это единственный возможный вариант, продиктованный порядком разрешения.

Изоляция схем — мощный инструмент. Но она заставляет думать о том, в каком порядке происходят события. Входящий звонок, вебхук от внешней CRM, запрос на подписание документа — все они приходят до того, как система знает, с кем имеет дело. И для каждого такого случая данные, нужные для идентификации клиента, должны лежать за пределами изолированных схем.

Мы перевернули подпись, когда поняли, что это не подпись

С документами похожая история — только здесь мы сначала сделали неправильно, а потом поняли почему.

Задача: клиент должен подписать договор электронной подписью. Мы сделали так: менеджер в CRM нажимает «отправить на подпись», система генерирует одноразовый код, менеджер видит этот код и передаёт клиенту. Клиент вводит код на публичной странице — договор подписан.

Всё работало. Демо проходило. Но через какое-то время до нас дошло: если менеджер знает код подписи, это не подпись. Он может ввести код за клиента. И никакая галочка в интерфейсе этого не предотвратит, потому что проблема не в интерфейсе, а в том, кто владеет секретом.

Пришлось перевернуть архитектуру. Теперь менеджер только отправляет клиенту ссылку. Код генерируется не в CRM, а на публичной странице — в момент, когда клиент сам нажимает «получить код». Менеджер не видит код и физически не может его узнать.

Это заняло время, сломало часть API и затронуло тесты. Но альтернативы не было. Если продукт обещает электронную подпись, имеющую юридическую силу, модель доверия важнее, чем удобство разработки. Это тот случай, когда «работает» и «правильно» — разные вещи.

Документ — это процесс, который может сломаться на каждом шаге

Договор кажется простым: шаблон, данные, PDF, подпись. Но когда делаешь это внутри продукта, а не в почте, каждый шаг — потенциальная точка отказа.

Шаблон редактируется в визуальном редакторе. Хорошо. Но поля должны подтягиваться из CRM — сумма, стороны, реквизиты. Если в сделке не заполнен адрес клиента, а шаблон его требует — PDF не соберётся. И это не ошибка кода, а неполнота данных, про которую менеджер должен узнать до отправки клиенту, а не после.

PDF генерируется на лету. Хорошо. Но генерация не должна висеть в HTTP-запросе — клиент ждать не будет. Значит, генерация — фоновая задача. А раз фоновая, нужно показывать статус и уметь перегенерировать при ошибке.

Клиент получает ссылку и подписывает. Хорошо. Но ссылка не должна жить вечно — у неё есть срок действия. А если клиент не подписал вовремя — нужна возможность переотправить.

И таких документов не один. Договор, акт, счёт, допсоглашение. У каждого свой шаблон, свой набор полей, свои правила. А живут они все внутри сделки и должны быть доступны в любой момент.

Это не rocket science. Но это тот класс задач, где дьявол кроется не в алгоритмах, а в состояниях. Документ проходит через десять рук и шесть статусов, и потерять его на полпути нельзя.

Что мы в итоге поняли

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

Электронная подпись — это не генерация кода и не проверка SMS. Это модель доверия. И если модель нарушена, никакой интерфейс не спасёт. Пришлось один раз переделать — теперь знаем, на что смотреть в первую очередь.

И ещё: в продукте, где собраны CRM, телефония, мессенджеры и документы, самое интересное происходит не внутри каждого компонента, а на их стыках. Звонок привязан к сделке. Сообщение в чате создаёт лид. Документ собирается из данных CRM. И если эти стыки не продуманы, продукт не работает — даже если каждый компонент по отдельности написан идеально.

prvms.crm в итоге — это платформа, где компания регистрируется и получает всё сразу: воронки и сделки, софтфон в браузере, документооборот с электронной подписью, мессенджеры, AI-ассистента. Но список фич никогда не расскажет, почему номера лежат в общем пространстве, а код подписи генерируется клиентом, а не менеджером.

А это, пожалуй, и есть самое интересное в разработке — не то, что сделано. А то, что пришлось понять по дороге.