Три месяца назад мы сидели на Bitrix24. Стандартный выбор для малого бизнеса в РФ: бесплатный тариф, CRM, лиды, воронка. Но когда наш AI-чат начал создавать по 20+ лидов в день и каждый из них шёл через webhook в облако 1С-Битрикс — стало ясно: мы не контролируем собственную базу клиентов.
Решили мигрировать на self-hosted CRM. Выбрали Twenty — open-source NestJS/GraphQL решение. Написали PHP-адаптер на 935 строк, настроили dual-write (две CRM работают параллельно) и мигрировали исторические данные. Ни одного потерянного лида. Вот как это устроено.
Почему ушли с Bitrix24
Bitrix24 — отличный продукт для тех кто работает «из коробки». Но как только начинаешь программировать интеграции — вылезают ограничения:
- Облачный webhook. API-ключ — это URL. Кто угодно с этим URL может создавать лиды. Нет ротации, нет scopes, нет audit log.
- REST API. Для создания лида с кастомными полями (lead_score, stage, industry) нужно 3 HTTP round-trip'а. На shared-хостинге это 2–3 секунды на заявку.
- Кастомные поля — UF_CRM_*. Именование полей привязано к user field ID. При переносе между порталами — ад.
- Vendor lock-in. Выгрузить данные можно только в Excel. API для массового экспорта нет. Миграция с Bitrix24 — это ручной труд.
После 3 месяцев эксплуатации стало ясно: нам нужна CRM, которую мы контролируем на уровне базы данных. Self-hosted, open-source, с нормальным API.
Почему Twenty CRM
Искали open-source CRM с GraphQL API и Docker-деплоем. Рассматривали:
- EspoCRM — хорош, но REST API и старый PHP-стек
- SuiteCRM — монстр на legacy коде, сложно допиливать
- Twenty CRM — NestJS + TypeORM + GraphQL, MIT-лицензия, Docker из коробки
Twenty выиграл по трём пунктам: GraphQL API (один запрос — все нужные поля), TypeORM (PostgreSQL а не MySQL), и MIT-лицензия (можно форкнуть и допилить под себя).
Архитектура адаптера: не переписывать, а обернуть
На сайте 6 PHP-файлов которые отправляют данные в Bitrix24: send_lead.php, chat_api.php, partner/api.php и так далее. Переписывать каждый — месяц работы и куча багов.
Вместо этого — написали один слой абстракции: crm_adapter.php. Он предоставляет те же функции что и Bitrix24 (crm_create_person, crm_create_opportunity, crm_add_note), но внутри вызывает GraphQL Twenty. Все 6 файлов подключили require_once 'crm_adapter.php' и заменили вызовы. Два дня работы вместо месяца.
9 транслированных методов
Адаптер переводит методы Bitrix24 в GraphQL-мутации Twenty:
| Bitrix24 | Twenty GraphQL | Статус |
|---|---|---|
| crm.lead.add | createPerson + createOpportunity | ✅ работает |
| crm.lead.update | updateOnePerson | ✅ работает |
| crm.deal.add | createOpportunity | ✅ работает |
| crm.timeline.comment.add | createNote + createNoteTarget | ✅ работает |
| crm.duplicate.findbycomm | findPeople query | ✅ работает |
| im.notify | email (нет push в Twenty) | |
| bizproc.workflow.start | нет аналога в Twenty | ⏸ skip |
Dual-write: ключевая идея
Самое опасное в миграции CRM — потеря лидов во время перехода. Решение: dual-write. Каждая заявка пишется одновременно в обе CRM.
// crm_adapter.php — конфигурация define('CRM_MODE', 'dual'); // twenty | bitrix | dual // Каждая функция адаптера: function crm_create_person($data) { $twenty_ok = twenty_create_person($data); $b24_ok = b24_create_lead($data); // Возвращаем успех если хоть одна CRM приняла лид return $twenty_ok || $b24_ok; }
Twenty упал (Docker перезагрузился, туннель отвалился)? Не страшно — лид всё равно в Bitrix24. Через месяц стабильной работы в dual режиме — переключаем CRM_MODE = 'twenty' и отключаем Bitrix24.
Twenty на локальном Docker а не на VPS. Почему? Потому что shared-хостинг не даёт PostgreSQL, а ставить CRM на тот же сервер что и сайт — плохая практика. Туннель (localtunnel) связывает хостинг с локальным Docker-контейнером. Да, добавляет ~3 секунды latency на запрос. Но для CRM это не критично — заявка всё равно обрабатывается асинхронно.
Дедупликация лидов
Самое интересное в миграции — поиск дубликатов. Bitrix24 ищет дубликаты по телефону/email через crm.duplicate.findbycomm. Twenty — через GraphQL query findPeople.
Адаптер ищет по:
- Телефон: local digits (последние 7–10 цифр, без форматирования)
- Email: точное совпадение (lowercase)
Если контакт уже существует — добавляется Note «Повторное обращение» вместо создания дубликата Person. При миграции исторических данных из 45 лидов Bitrix24 было создано 29 в Twenty, 16 пропущено как дубликаты.
Инфраструктура: Docker + туннель + мониторинг
Twenty живёт в Docker на Windows-машине. Состав:
- twenty-server — NestJS на порту :3001, GraphQL API
- twenty-worker — фоновая очередь (Redis-backed)
- postgres-16 — основная БД
- redis — очередь задач и кэш
Доступ из интернета — через localtunnel. crm_url.txt на хостинге динамически обновляется скриптом twenty_tunnel.ps1 при перезапуске туннеля. PHP-адаптер читает этот файл чтобы знать актуальный URL.
Мониторинг: twenty_monitor.ps1 каждые 30 минут проверяет Docker-контейнеры, healthz-эндпоинт и состояние туннеля. При падении — алерт.
Результаты и планы
Что получили:
- 0 потерянных лидов при миграции — dual-write отработал без сбоев
- 935 строк PHP-адаптера — заменили все вызовы Bitrix24 в 6 файлах сайта
- 29 лидов перенесено из Bitrix24 в Twenty с дедупликацией
- Полный контроль над данными — PostgreSQL на своём железе, MIT-лицензия
Что дальше:
- После месяца стабильной работы — отключить Bitrix24 совсем (
CRM_MODE = 'twenty') - Перенести Twenty на выделенный VPS для продакшена
- Добавить Telegram-уведомления оператору (сейчас — email)
- Настроить кастомную воронку продаж (10 этапов)
Главный вывод из этого проекта: миграция CRM — это не про «перенести данные из А в Б». Это про обеспечение непрерывности бизнеса в момент перехода. Dual-write — простой но эффективный паттерн, который даёт время на стабилизацию новой системы без риска потери лидов. Рекомендую всем кто планирует переезд с одного вендора на другого.