# ADR-0001: Разделение Chat Platform на 3 сервиса Дата: 2026-02-21 Статус: accepted ## Контекст Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`. Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга. Критичные требования: - входящие webhook-события не теряются при рестартах; - delivery управляет retry/rate-limit централизованно; - omni_chat остается единственным местом доменной логики и хранения состояния диалогов; - сервисы можно обновлять независимо. ## Решение Принимаем разделение на 3 сервиса: 1. `omni_inbound` - Принимает вебхуки провайдеров. - Валидирует подпись/секрет. - Нормализует событие в универсальный envelope. - Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`. - Возвращает `200` только после успешной durable enqueue. - Не содержит бизнес-логики CRM. 2. `omni_chat` - Потребляет входящие события из `receiver.flow`. - Разрешает идентичности и треды. - Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты. - Формирует исходящие команды и кладет их в `sender.flow`. 3. `omni_outbound` - Потребляет `sender.flow`. - Выполняет отправку в провайдеров (Telegram Business и др.). - Управляет retry/backoff/failover, DLQ и статусами доставки. - Не содержит UI и доменной логики чатов. ## Почему webhook и delivery разделены - Входящий контур должен отвечать быстро и предсказуемо. - Исходящий контур живет с долгими retry и ограничениями провайдера. - Сбой внешнего API не должен блокировать прием входящих сообщений. ## Границы ответственности `omni_inbound`: - можно: auth, валидация, нормализация, дедуп, enqueue; - нельзя: запись доменных сущностей CRM, принятие продуктовых решений. `omni_chat`: - можно: вся доменная модель чатов, orchestration, бизнес-правила; - нельзя: прямые вызовы провайдеров из sync API-контекста. `omni_outbound`: - можно: провайдерные адаптеры, retry, rate limits; - нельзя: резолвинг бизнес-правил и маршрутизации диалога. ## Универсальный протокол событий Внутренний контракт входящих событий: `docs/contracts/omni-inbound-envelope.v1.json`. Обязательные поля: - `version` - `idempotencyKey` - `provider`, `channel`, `direction` - `providerEventId`, `providerMessageId` - `eventType`, `occurredAt`, `receivedAt` - `payloadRaw`, `payloadNormalized` ## Идемпотентность и надежность - `jobId` в очереди строится из `idempotencyKey`. - Дубликаты входящих webhook событий безопасны и возвращают `200`. - `200` от `omni_inbound` отдается только после успешного добавления в Redis/BullMQ. - При ошибке durable enqueue `omni_inbound` возвращает `5xx`, провайдер выполняет повторную доставку. - Базовые рабочие очереди: `receiver.flow` и `sender.flow`; технические очереди для эскалации: `receiver.retry`, `sender.retry`, `receiver.dlq`, `sender.dlq`. ## Последствия Плюсы: - независимые релизы и масштабирование по ролям; - меньше blast radius при инцидентах; - проще подключать новые каналы поверх общего контракта. Минусы: - больше инфраструктурных компонентов (очереди, мониторинг, трассировка); - требуется дисциплина по контрактам между сервисами. ## План внедрения 1. Вводим `omni_inbound` как отдельный сервис для Telegram Business. 2. Потребление `receiver.flow` реализуем в `omni_chat`. 3. Текущее исходящее API оставляем за `omni_outbound`. 4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`.