Когда я начинал этот проект, задача выглядела довольно понятно.
Нужно было сделать код для одного автосервиса. Предполагалось, что затем владельцы других сервисов смогут взять open-source продукт, развернуть его у себя и пользоваться им самостоятельно: вести клиентов, автомобили, заказы, документы и работу сотрудников.
Для первого этапа такая модель была естественной. Она позволяла сосредоточиться на самом продукте, а не на платформе вокруг него. У каждого сервиса была бы своя установка, свои данные, своё окружение и свой темп обновлений.
Но по мере работы стало видно, что продукт идёт в другую сторону.
Нам нужен был не набор независимых установок, а единый сервис для многих автосервисов. Такой, где организация создаёт своё рабочее пространство, получает изолированные данные и обновления без отдельного развёртывания каждой новой версии.
Так проект перешёл от open-source модели к мультитенантному SaaS.
На словах это иногда звучит как небольшая архитектурная задача: добавить организациям идентификаторы, разделить данные, настроить роли. На деле пришлось пересмотреть почти все границы продукта.
Старая модель перестала отвечать на новые вопросы
Установка для одного автосервиса и SaaS для множества организаций могут выполнять похожие функции. В обоих случаях есть клиенты, автомобили, заказы, сотрудники и документы.
Но ответственность устроена по-разному.
В самостоятельной установке каждый владелец сервиса отвечает за своё окружение. Он решает, когда обновляться, как поддерживать инфраструктуру и что делать, если версия продукта расходится с другими установками.
В SaaS система одна, но внутри неё работают независимые организации. Они не должны видеть данные друг друга, влиять на чужие процессы или получать доступ к действиям, которые не относятся к их роли.
Из-за этого появились вопросы, которые нельзя было оставлять на потом:
- как пользователь создаёт организацию и попадает в нужный автосервис;
- кому принадлежат автомобили, заказы и документы;
- что видит владелец автомобиля;
- какие действия доступны сотруднику сервиса;
- что может делать механик;
- где заканчиваются права организации и начинаются полномочия самой платформы;
- как выпускать обновления одновременно для всех, не превращая каждый релиз в отдельный проект.
Пока продукт рассчитан на одну установку, часть этих вопросов легко не заметить. Когда внутри одной системы появляются разные организации, неявные договорённости становятся риском.
Я понял, что недостаточно добавить к старой модели несколько новых полей и назвать результат мультитенантностью. В таком случае старый продукт остаётся внутри нового, а вокруг него постепенно растут исключения, флаги и переходники.
Через несколько месяцев уже трудно понять, что относится к текущей модели, а что осталось от прежней.
Не добавлять слой сверху, а поменять основу
Главное решение здесь было не техническим, а продуктовым.
Проект всё ещё находился в разработке. Не было клиентского трафика и данных, ради которых пришлось бы годами поддерживать прежнюю логику. Поэтому я выбрал более прямой путь: не строить совместимость с тем, что больше не нужно.
Если старая сущность или сценарий противоречили новой модели, их не нужно было сохранять «на всякий случай». Их можно было убрать.
Это не сделало работу маленькой. Зато она стала честнее.
Вместо вопроса «как сохранить все старые сценарии?» появился другой:
Какая модель нужна продукту сейчас и какие границы в ней нельзя размывать?
Основой стала понятная последовательность: исходный запрос превращается в сервисный запрос, затем в заказ конкретного автосервиса. История изменений заказа сохраняется отдельно, чтобы новые правки не переписывали прошлое задним числом.
Когда процесс определён, проще задать остальные границы:
- кому принадлежит заказ;
- кто имеет право его увидеть;
- кто меняет статус;
- что может делать сервис;
- что относится к работе механика;
- какие данные должен видеть владелец автомобиля;
- где проходит граница между организациями.
Это не универсальная схема для любого продукта. Но для нашей платформы она стала опорой, от которой можно проверять решения.
Разные роли не должны жить в одном кабинете
На ранней стадии продукта хочется собрать всё в одном интерфейсе. Кажется, что так быстрее: один фронтенд, одна навигация, а лишние пункты меню можно скрывать в зависимости от роли.
Проблема в том, что скрытое меню не создаёт границ.
Владелец автомобиля, сотрудник сервиса, механик и платформенный администратор смотрят на один и тот же заказ по-разному. У них разные задачи, разный объём информации и разная цена ошибки.
Поэтому я разделил эти сценарии.
Публичная часть рассказывает о сервисе и ведёт человека в нужный путь.
Кабинет владельца автомобиля нужен для его машин, заявок и истории обслуживания.
Рабочее пространство автосервиса предназначено для заказов, клиентов, автомобилей, документов, сотрудников и внутренних процессов организации.
Рабочее место механика намеренно уже. Механик должен видеть назначенные ему работы, а не всю коммерческую и клиентскую информацию сервиса.
Отдельно существует платформенное администрирование. Это не кабинет автосервиса с дополнительными кнопками, а контур управления самой платформой.
Такое разделение добавляет работы на старте. Зато у каждого интерфейса появляется простой вопрос:
Что этот человек должен сделать здесь сейчас?
Если на него нет ясного ответа, экран или действие, вероятно, лишние.
Роль — это не настройка интерфейса
При переходе к SaaS легко принять визуальные ограничения за настоящую защиту.
Например, можно скрыть от механика кнопку редактирования. Это полезно для интерфейса, но ничего не гарантирует, если сервер всё равно принимает запрос.
Поэтому правила должны работать не только на экране. Система сама определяет, кто выполняет действие, к какой организации относится пользователь, в каком рабочем контексте он находится и имеет ли право на конкретный запрос.
Интерфейс помогает человеку не ошибиться. Окончательное решение остаётся за сервером.
То же относится к данным. Если механику не нужны финансовые сведения, лишние данные клиента или служебные поля, их не стоит просто прятать в интерфейсе. Они не должны попадать в его рабочий набор данных вовсе.
Это не попытка усложнить продукт. Это нормальная дисциплина для системы, где рядом работают разные организации и разные роли.
Почему я не стал переписывать всё одним заходом
Большие переделки часто ломаются не потому, что команда не умеет писать код. Они ломаются, когда изменения невозможно проверить по частям.
Поэтому переход я разделил на шесть волн разработки.
Сначала нужно было зафиксировать доменную модель и убрать то, что конфликтовало с ней. Затем отдельно выстраивались сценарии доступа, жизненный цикл заказов, интерфейсы разных ролей, операционный контур и выпуск.
У каждой волны был свой проверяемый вопрос.
Не «готов ли уже весь продукт?», а, например:
- работают ли организации только со своими данными;
- не остались ли активные сценарии прежней модели;
- можно ли получить доступ к чужому заказу прямым запросом;
- не выдаёт ли публичная ссылка лишнюю информацию;
- сохраняется ли история изменений;
- видит ли механик только назначенные ему работы;
- можно ли развернуть новую версию и подтвердить её состояние без ручных действий на сервере.
Каждая волна должна была заканчиваться не отчётом о проделанной работе, а доказательствами: тестами, сборкой, проверкой схемы данных, проверкой прав доступа, отсутствием устаревших маршрутов и проверкой выпуска.
AI-агенты здесь заметно ускорили работу. Они помогали параллельно исследовать разные части проекта, находить старые зависимости, готовить проверки и проверять достижимость маршрутов.
Но направление и приёмка оставались за мной.
Агент может предложить изменение или сообщить, что задача завершена. Он не решает, какую старую сущность нужно сохранить, где проходит продуктовая граница между ролями и достаточно ли доказательств перед выпуском. Его отчёт остаётся гипотезой, пока я не проверю код, тесты и фактическое поведение системы.
Временные решения быстро становятся постоянными
Во время большого перехода постоянно хочется сказать: «Пока оставим старое, потом разберёмся».
Иногда это необходимо. Если в системе уже есть пользователи, исторические данные и обязательства, переход должен быть осторожным.
Но когда продукт ещё можно менять свободно, временная совместимость быстро обрастает постоянными ветками.
Остаётся старый маршрут «на всякий случай». Прежняя сущность продолжает использоваться в одном экране. Новый сценарий начинает принимать два формата данных. В интерфейсе появляется переключатель между старым и новым поведением.
Через несколько месяцев трудно объяснить, что из этого действительно нужно продукту, а что просто осталось от незавершённого переезда.
Я старался не поддерживать старую модель там, где она мешала новой. Это помогло упростить не презентацию, а реальное поведение системы: убрать лишние ветки, исключения и правила, которые держатся только на памяти людей, работавших с проектом раньше.
Production — тоже часть продукта
После того как код написан и тесты проходят, легко решить, что работа закончена.
На самом деле начинается другой этап.
Для SaaS недостаточно, чтобы проект собирался на локальной машине. Нужно понимать, как он запускается, где хранятся данные, какие сервисы доступны извне, как разделены окружения, что произойдёт при ошибке и как подтвердить, что новая версия действительно работает.
Поэтому выпуск я проверял отдельно.
До него были резервные копии и подготовленный путь отката. После него — состояние процессов, доступность публичных точек входа, целостность базы и отсутствие открытых наружу внутренних сервисов.
Это не самая заметная часть работы. Её не покажешь красивым скриншотом. Но именно здесь становится понятно, есть ли перед тобой продукт или просто набор исходников.
Переход на мультитенантную модель завершён. Платформа работает как единый SaaS-продукт, а не как кодовая база для множества самостоятельных установок.
Что дальше
На этом работа не закончилась.
Переход на SaaS решил одну большую задачу: теперь продукт построен как единая платформа с разделёнными организациями, ролями и рабочими сценариями.
Но архитектурная готовность не означает, что интерфейс уже идеален.
Сейчас я отдельно переделываю UI. Это другая работа: как сделать сложную систему понятной для владельца автомобиля, сотрудника сервиса, механика и администратора платформы. Когда там появится материал, которым можно делиться, я расскажу об этом отдельно.
Эта история для меня не только про автосервисы и мультитенантность.
Она про момент, когда нужно признать: первоначальная версия продукта выполнила свою задачу, но больше не подходит следующей версии замысла.
В такой момент не всегда нужно аккуратно развивать то, что уже есть. Иногда честнее заново определить основу, разделить переход на проверяемые части и оставить в системе только то, что действительно нужно будущему продукту.
Если вы тоже переводите продукт из самостоятельных установок в единый сервис, напишите мне. Я помогу разобрать границы данных, ролей, сценариев доступа и выпусков, которые стоит определить до того, как переход обрастёт временной совместимостью.
