Когда заменить внешний сервис своей инфраструктурой: кейс миграции WhatsApp-шлюза за один день
Я уволил сервис, который три года был в центре нашей операционки. wa-sms.com — внешний WhatsApp-шлюз, через который ходили все сообщения гостям и уборщикам 16 активных вилл: заселения, расписания уборок, отчёты по объектам, координация с персоналом. Каждый день, каждое бронирование. Однажды утром зашёл в настройки, удалил номер Solar Property из wa-sms — и всё немедленно сломалось. Исходящие гостям перестали уходить, входящие от уборщиков перестали приходить, цепочка из 4 скриптов начала молча падать.
Это было запланировано. Месяц мы строили замену — собственный WhatsApp-узел на Baileys, который уже работал у нас для модерации чатов аренды. Переключение прошло через 3 фазы, суммарно заняло один рабочий день и сохранило 1500 строк бизнес-логики без единого касания. Вот как именно это устроено и, главное, как понять когда такой переезд вообще стоит делать.
Важная оговорка: я не призываю строить всё самому. Внешние сервисы существуют именно для того, чтобы снижать порог входа и освобождать ресурсы от инфраструктурных задач. wa-sms был правильным решением в 2023 году — быстрый старт, готовый API, не нужно держать собственный WhatsApp-узел. Вопрос не «строить или платить», а «когда именно менять». И у этого вопроса есть конкретные ответы.
Этот день был нетипичным не тем, что мы что-то сломали — а тем, что за один день одновременно завершили инфраструктурную миграцию, запустили модераторский апгрейд для 4 чатов суммарно более 1770 участников, сделали ban-by-phone инструмент, открыли публичную community-страницу и переупаковали оффер для клиента. Такое количество завершённых задач за рабочий день — следствие именно архитектурных решений, которые разберём ниже: когда переход не требует переписывать тысячи строк, он перестаёт быть страшным.
Как зависимость от внешнего сервиса выглядит изнутри
wa-sms не был плохим продуктом. В 2023 году, когда мы начали автоматизировать операционку, это было правильным решением: быстрый старт, готовый API, платишь и не думаешь об инфраструктуре. Именно так и должны работать внешние сервисы на старте — снижать порог входа и давать возможность сосредоточиться на бизнес-логике, а не на техническом стеке.
Проблема появилась не в цене и не в надёжности. Проблема — в контроле. Три конкретных сигнала, по которым я понял что пора строить собственное:
Первый: невозможность добавить интерактивность. Baileys, наш собственный WhatsApp-узел для модерации чатов, умел то, чего wa-sms физически не мог — интерактивные сообщения с кнопками, листы меню, реакции на события в чате. Каждый раз, когда нужно было добавить диалог с ответными опциями для гостя или уборщика, упирались в ограничения внешнего API. Это не баг wa-sms, это архитектурный потолок продукта, который изменить нельзя.
Второй: два разных формата идентификаторов. Baileys идентифицирует пользователей через JID-строку вида [email protected]. wa-sms работает с обычным телефонным номером в формате +79001234567. Каждый скрипт, который касался обеих систем, нёс внутри логику конвертации — написанную по-разному, без единого стандарта, с разной обработкой крайних случаев. Это архитектурный мусор, который накапливается с каждой новой интеграцией и делает код всё сложнее сопровождать.
Третий: чужой баг — ваша проблема. Один раз wa-sms не доставил сообщение уборщику о заселении. Не наш баг, не наша ошибка — но наш гость встал перед закрытой виллой. Внешний сервис означает: если у них авария в 23:00 — вы просто ждёте. Для операционки с живыми гостями и реальными бронированиями это неприемлемый уровень зависимости от чужой инфраструктуры.
Когда все три сигнала появились одновременно — вопрос стал не «менять или нет», а «насколько сложно перейти именно сейчас». Ответ оказался: один день. Именно поэтому переехали.
Архитектура миграции: три фазы без касания бизнес-логики
Главный страх при замене глубоко встроенного сервиса — объём работы. В нашем случае 4 скрипта использовали wa-sms для исходящих сообщений. Входящие от уборщиков обрабатывались ещё через несколько обработчиков в разных частях системы. Суммарно — около 1500 строк бизнес-логики, которую в лобовом подходе пришлось бы полностью переписывать под новый формат Baileys.
Мы не тронули ни одной из этих строк. Вот как именно.
Фаза 1: расширение схемы базы данных
Первая проблема была не в коде, а в данных. В нашей базе телефонные номера хранились в формате +79001234567. Baileys для отправки требует JID-строку в формате [email protected]. Каждый из 4 скриптов конвертировал номер в JID на лету — своим способом, без единого стандарта, с разной обработкой форматов с плюсом и без плюса, с разными крайними случаями для нестандартных номеров.
Решение: добавить в таблицу contacts новый столбец whatsapp_jid. Заполнить его автоматически для всех существующих записей через migration-скрипт. Теперь любой скрипт получает готовый JID прямо из базы — не вычисляет сам, не рискует ошибиться в конвертации. Миграция заняла около 40 минут, включая проверку 312 контактов с нестандартными форматами и ручную валидацию крайних случаев.
Параллельно в схему добавили индекс по JID для быстрого поиска — поскольку входящие сообщения от Baileys приходят именно с JID, и нам нужно быстро матчить их к нашим контактам без полного сканирования таблицы.
Фаза 2: универсальный handler для исходящих
Четыре скрипта отправляли сообщения через wa-sms по-разному: один через requests напрямую к API, второй через обёртку в utils, третий через специальную функцию отправки, четвёртый — через HTTP-клиент из другого модуля. Каждый знал адрес wa-sms и формат запроса. При смене транспорта каждый нужно было бы обновить отдельно, тестировать отдельно, отлаживать отдельно.
Вместо того чтобы обходить каждый из 4 скриптов, написали один универсальный handler: send_wa_message(phone, text, media=None). Внутри — логика выбора транспорта через флаг в конфиге USE_BAILEYS. Когда флаг False — сообщение уходит через wa-sms. Когда True — через Baileys. Все 4 скрипта заменили свои вызовы на единый send_wa_message(). Тестирование нового транспорта без риска для production: просто переключаем флаг и смотрим на 10 тестовых сообщениях.
Побочный эффект, который оказался важнее самой миграции: теперь у нас единая точка логирования всех исходящих WhatsApp-сообщений. До этого каждый скрипт логировал по-своему — или не логировал вообще. Теперь каждое отправленное сообщение, его статус доставки, время отправки и получатель — в единой таблице wa_outgoing_log. Это и дебаг, и аудит, и метрики за один заход.
Фаза 3: адаптер для входящих
Входящие сообщения — сложнее исходящих. wa-sms присылал вебхуки в своём JSON-формате: конкретная структура полей, конкретные названия ключей, конкретный формат времени. Baileys присылает события в другом формате — другие имена полей, другая структура вложенности, другой способ передачи медиафайлов. Около 1500 строк обработчиков в нашей системе ожидали именно формат wa-sms.
Переписывать 1500 строк — это минимум неделя работы и высокий риск регрессий в production. Мы написали адаптер-прослойку в 80 строк. Baileys-событие поступает к адаптеру, адаптер преобразует его в формат wa-sms и передаёт дальше — в существующие обработчики. С их точки зрения ничего не изменилось: они по-прежнему получают сообщения в привычном JSON-формате с привычными именами полей. Адаптер — единственное новое, что появилось в системе.
Соотношение: 80 строк нового кода вместо потенциальных 1500 строк правок. Именно это соотношение делает миграцию «за один день» реальной, а не маркетинговым преувеличением.
Финальный переключатель — флаг USE_BAILEYS = True в конфиге — пока не дёрнут. WhatsApp разморозит бренд-номер после отвязки от wa-sms за 6-8 часов. Вся инфраструктура готова, протестирована на staging-окружении, ждёт одного изменения в конфигурационном файле.
Параллельно: ребрендинг и апгрейд модератора 4 чатов аренды
Пока шла миграция инфраструктуры, параллельно шёл ребрендинг и функциональный апгрейд наших WhatsApp-чатов аренды. У Solar Property 4 группы по бюджетам: 0-5 миллионов рупий в месяц, 5-10 миллионов, 11-20 миллионов и 21-99 миллионов. До этого дня каждый чат жил сам по себе: разные названия, разные аватарки, никакой перекрёстной навигации между бакетами.
Проблема очевидна: человек ищет виллу за 8 миллионов рупий в месяц, попадает в чат 5-10, размещает объявление — и не знает, что для 8 миллионов логичнее чат 11-20. Мы теряем качество объявлений и потенциально аудиторию только из-за отсутствия навигации между чатами.
Теперь все 4 чата переименованы под единый бренд «Bali Villas», получили единый визуальный стиль и закреплённые сообщения с явными ссылками на соседние бакеты. Закреп содержит: диапазон бюджета чата, ссылки на чаты меньшего и большего бюджета, правила модерации одной строкой на английском и индонезийском.
Модератор получил три новых функции, которые меняют логику взаимодействия с аудиторией:
Price detection с grace period. Если пост с объявлением виллы не содержит цены — бот публично отвечает автору с тегом и даёт 2 минуты на правку. Если автор делает edit и добавляет цену в течение этого времени — бот замечает изменение и не удаляет пост. Раньше такая логика требовала ручного контроля или молчаливого удаления без объяснений.
Smart redirect вместо молчаливого удаления. Если вилла за 25 миллионов попадает в чат 11-20 — бот не просто удаляет пост без объяснений. Бот пишет автору: «твой пост точнее зайдёт в чат 21-99, вот прямая ссылка». Это разница между модерацией как наказанием и модерацией как сервисом.
Кросс-чатовый бан. 3 предупреждения в любом из 4 чатов — авто-бан из всех четырёх сразу. До этого каждый чат был изолирован: можно было получить бан в одном и продолжить спамить в трёх других. Теперь черный список единый для всего бренда «Bali Villas».
Ban-by-phone: от ручного UI к одной команде
Параллельно с модератором собрали отдельный инструмент — ban-by-phone бот. Работает просто: пишешь в личный канал телефонный номер — бот за секунду вытаскивает участника из всех 4 чатов и заносит в глобальный чёрный список. Статус операции возвращается обратно: из каких чатов удалён, добавлен ли в blacklist, были ли ошибки.
Раньше бан делался через UI мессенджера — вручную, последовательно, в каждом чате отдельно: найти участника в списке, открыть профиль, выбрать действие, подтвердить. Умножить на 4 чата. Это 4-5 минут при хорошем знании интерфейса, и «непонятно как» для тех, кто с WhatsApp-группами работает редко. Теперь весь процесс — 5 секунд и одна команда.
К каждому подозрительному посту в мой личный канал прилетает карточка с двумя кнопками: «удалить пост» и «забанить везде». В сами чаты руками не лезу — всё через интерфейс бота. Это важно: когда в операционке несколько чатов с разными людьми, прямое вмешательство в UI мессенджера — это источник ошибок.
Отдельный результат того же дня — инвентаризация inactive-участников. Экспортнул историю 3 чатов в текстовый файл, прогнал парсером статистики сообщений: суммарно 1770 участников в трёх чатах написали за всю историю ровно одно сообщение или не написали ни одного. В самом большом чате до лимита WhatsApp в 1024 участника оставалось совсем немного свободных мест. Вычистка 1770 кандидатов — не наказание, а освобождение мест под тех, кто реально пользуется чатом.
Community-страница: публичная оферта как SEO и страховка
Описание группы в WhatsApp лимитировано по символам. У нас 4 публичных WhatsApp-чата плюс Telegram-зеркала — хотелось одно место со всеми правилами, ссылками и юридическим позиционированием. Решение: отдельная страница на сайте компании.
Сделали solarpropertybali.com/community/ — на русском и английском. На странице: список всех чатов с описанием бюджетных диапазонов и прямыми ссылками, блок «как мы модерируем» с конкретными примерами действий бота, и публичная оферта на 7 пунктов. Ключевой пункт: Solar Property является хостом площадки, а не стороной сделки между арендодателем и арендатором.
Это решает три задачи одновременно. SEO-точка для тех, кто ищет чаты аренды вилл на Бали — по ключевым словам «WhatsApp чат аренда вилл Бали», «Bali Villas chat», «чат аренды недвижимости Бали». Единое место для актуализации правил: вместо правки 4 закрепов в 4 чатах одновременно — одна страница, одно обновление. Юридическая страховка на случай «вы нам обещали» или «вы несёте ответственность» — страница явно описывает роль платформы и границы ответственности.
B2B-инсайт: milestone-pricing вместо скидки
Отдельная история того же дня — разговор с клиентом по медицинскому проекту. Исходное предложение: 180 тысяч рублей за внедрение системы автоматизации. Клиент заинтересован, компетентен, понимает что задача нужна — но cashflow зажат до середины лета. Такая сумма авансом в текущей ситуации не лезет.
Стандартный ответ в такой ситуации — скидка или ожидание. Оба варианта теряют либо выручку, либо время. Я предложил третий вариант: milestone-pricing.
Вместо полного внедрения за 180 тысяч — один модуль за 90 тысяч, разбитый на 3 равных платежа по 30 тысяч. Привязка не к календарю, а к проверяемым событиям: первый платёж при старте работ, второй при запуске пилота в production через 3 недели, третий после 30 дней работы на реальных цифрах. На любой стадии клиент может остановиться, потеряв максимум 30 тысяч — стоимость одного завершённого этапа.
Это принципиально другая структура риска для обеих сторон. Клиент не берёт обязательство на 90 тысяч заранее — он берёт обязательство на 30 тысяч и проверяет реальный результат перед следующим шагом. Я получаю старт проекта вместо отказа, и при этом не теряю в итоговой выручке — три платежа по 30 это те же 90 тысяч.
Большой оффер пугает не ценой, а неопределённостью результата. Разбивка на этапы с конкретным проверяемым результатом за каждый снимает страх — и при этом не снижает итоговую ценность для вас как исполнителя. Именно эту модель я стал применять в последние несколько месяцев в ситуациях, где клиент технически готов, но психологически не готов к большому авансу.
Критерии: когда строить самому, а когда платить внешнему сервису
По итогу этого дня могу сформулировать критерии, по которым я принимаю решение о замене внешнего сервиса. Это не теория — это конкретная рабочая матрица, выведенная из реального опыта.
Платишь внешнему сервису, пока выполняется хотя бы одно из:
- Интеграция заняла часы, а не недели — сервис снизил порог входа, как и должен был.
- Возможности сервиса покрывают ваши потребности с запасом, без обходных решений.
- Баг у них — это их проблема, а не ваша операционная авария.
- Стоимость сервиса меньше зарплаты разработчика на поддержку аналогичного стека.
- У вас нет смежной инфраструктуры, которую можно расширить до нужных функций.
Строишь сам, когда:
- Сервис ограничивает то, что критически нужно — кнопки, форматы, скорость — и обходного пути нет.
- Вы уже держите аналогичный стек для другой задачи. В нашем случае: Baileys уже работал для модерации чатов. Расширить его до функции шлюза — это не «построить с нуля», это «добавить роль существующему компоненту».
- Потеря контроля стоит дороже, чем инженерный день. Авария у внешнего сервиса в 23:00 с живыми гостями — стоимость зависимости измеряется не деньгами, а репутацией.
- Миграция может быть сделана через адаптерный слой без переписывания бизнес-логики. Это самое важное условие: если для перехода нужно трогать 1500 строк — это месяц работы. Если нужно написать 80-строчный адаптер — это один день.
Последний критерий — ключевой. Вопрос не «стоит ли переезжать», а «насколько сложно переезжать именно сейчас при текущем состоянии архитектуры». Хорошо написанные системы позволяют замену транспортного слоя без касания бизнес-логики. Плохо написанные — требуют переписывать всё при каждом изменении инфраструктуры. Инвестиция в адаптерные слои и единые точки входа окупается не при первой миграции, а при каждой последующей.
В нашем кейсе с wa-sms ответ оказался «один рабочий день». Именно поэтому мы переехали в конкретный день, а не откладывали на «когда-нибудь потом». Три месяца назад ответ был бы «три недели» — потому что не было Baileys-инфраструктуры рядом. Сейчас была — и это изменило всё.
Если вы строите операционную автоматизацию и хотите не попасть в ту же ловушку зависимостей — смотрите не только на то, что сервис умеет сейчас, но и на то, что вы не сможете сделать, когда он перестанет справляться. Архитектурный долг от неправильно выбранных внешних зависимостей накапливается медленно, но платить по нему приходится в самый неудобный момент.
Следующий вопрос — логичный: сколько у вас в операционке сервисов, без которых всё встанет завтра? И сколько из них вы могли бы заменить собственным стеком за один рабочий день, если бы уже держали нужную инфраструктуру рядом? Это не призыв строить всё самому — это призыв знать ответ на этот вопрос. Сервис, без которого всё встанет и который вы не можете заменить — это риск, который живёт в вашем бизнесе каждый день, независимо от того, думаете вы о нём или нет.
О том, как мы выстраиваем автоматизацию операционки через WhatsApp-вебхуки и уведомления, я писал подробно в статье про WhatsApp-вебхуки и автоматизацию уведомлений. А про принятие решений об автоматизации без лишних инвестиций — в реальном кейсе автоматизации операционки на Бали.