Multi-tenant интеграция BOTIX: ключи, события, изоляция
TL;DR: если вы ведёте сотню проектов на одной платформе, ключи, границы доверия и способ получения событий перестают быть настройкой и становятся архитектурой. Разберём три решения, которые определяют, переживёт ли интеграция рост: изоляция `project_id` и один SDK-инстанс на проект, осознанный выбор между polling и webhook, и хранение API-ключей без семи типовых утечек. Все факты — по реальному Public API BOTIX (`api.botix.pro/public/v1`).
Коротко
- Multi-tenant — это дисциплина изоляции, а не размер кодовой базы. Один ключ на
project_id, один SDK-инстанс на проект, минимальный scope на каждый сервис, ротация черезrevoked-состояние без даунтайма. - Polling vs webhook — не вопрос вкуса, а замер трафика и задержки. Нужна задержка < 5 секунд при живом публичном endpoint — webhook; cron раз в час или локальная разработка без туннеля — polling через cursor.
- Утечки ключей — это семь повторяющихся антипаттернов, а не хитрый взлом. Секрет-менеджер → ENV → SDK, маскирование в логах, никаких ключей в репозитории, URL и Slack.
Комбинация трёх тем — изоляция арендаторов, доставка событий и хранение секретов — это и есть «архитектура интеграции». По отдельности каждая выглядит как деталь. Вместе они решают, работает ли интеграция только до первого инцидента или переживает рост со ста до тысячи проектов. Разберём каждую как отдельное решение с кодом и опорой на реальный API.
Один SDK на сотню проектов: границы доверия и изоляция project_id
Сценарий: агентство строит ботов на BOTIX и ведёт сотню клиентов. Со стороны платформы это сотня отдельных проектов — своя база контактов, свои сценарии, свои лимиты по тарифу. Со стороны агентства — одна команда, один репозиторий, один деплой. Между этими двумя реальностями нужен мост, который не утечёт из одного клиента в другого, переживёт ротацию ключей без даунтайма и не превратится в куст if'ов по проектам.
Большинство ошибок здесь лежит не в коде, а в неявных допущениях: «project_id один на запрос», «токен можно положить в общий конфиг», «лимит у всех одинаковый». Каждое однажды оказывается неверным — и тогда клиент A видит данные клиента B, либо вся интеграция упирается в лимит самого слабого тарифа из ста.
Границы доверия и способ аутентификации. В multi-tenant интеграции три зоны: интегратор, платформа, клиент платформы. Ключи должны явно отражать эти границы. BOTIX даёт два паттерна:
- API-ключ на проект. Интегратор хранит по одному ключу на клиента, ключи выпущены через клиентский кабинет. Просто и предсказуемо, но на каждого нового клиента — ручной шаг: клиент заходит в
app.botix.pro/settings?tab=api-keys, генерирует ключ, отдаёт. - OAuth installation. Интегратор регистрирует своё приложение, получает
client_id/client_secret, любой клиент платформы одним кликом «подключает приложение к моему проекту». На выходе —access_token, привязанный одновременно к приложению и к конкретному проекту. Реализовано черезbtx_oauth_installationsи страницу согласияapp.botix.pro/oauth/authorize. Это стандарт для публичных third-party интеграций (GitHub Apps, Stripe Connect). Развилку «когда ключ, когда OAuth» разбираем отдельно — OAuth 2.0 vs API-key в SaaS-интеграциях.
Partner API (создание проектов от имени партнёра) — в roadmap'е без публичного релиза.
Изоляция project_id. Главное требование multi-tenant — невозможность утечки между проектами. На платформе это жёсткая проверка: токен резолвится в project_id, каждый контроллер после получения $projectId вызывает Auth::assertProjectAccess($projectId), любой SQL содержит WHERE project_id = ?. При попытке достать чужой ресурс платформа отвечает 404, не 403 — чтобы не утекало само существование объекта.
На стороне интегратора нужна такая же дисциплина. Если SDK инициализируется один раз глобально и переиспользуется во всех ботах — любая ошибка в параметрах уходит запросом на чужой проект. Правильный паттерн — один инстанс SDK на проект:
from botix import BotixClient
clients = {
project_id: BotixClient(api_key=XXXXXXXX(project_id))
for project_id in active_projects
}
def send_to_project(project_id: int, contact_id: int, text: str):
client = clients[project_id]
client.messages.create(contact_id=contact_id, text=text)Ключи берутся из защищённого хранилища, инстансы кэшируются на старте процесса, и в момент вызова невозможно перепутать project_id с api_key. Если у одного project_id ключ ротируется — пересоздаётся только его инстанс, остальные работают.
Scopes. По умолчанию ключ может всё, что доступно тарифу. На практике это редко нужно: сервису отправки сообщений не нужны права на удаление контактов или чтение журнала транзакций. BOTIX поддерживает scopes через поле scopes (JSON) в btx_api_keys и btx_oauth_access_tokens. При создании ключа на app.botix.pro/developers указывается список разрешённых scope (contacts:read, messages:send, analytics:read, …), и middleware Public API на каждом запросе проверяет соответствие. Несоответствие — 403 INSUFFICIENT_SCOPE. Значит: на каждый изолированный кусок логики — отдельный ключ с минимальным scope.
Ротация без даунтайма. Плановый ротейт раз в полгода — гигиена, внеплановый после утечки — обязателен. У ключа два состояния: active и revoked (поле status + revoked_at в btx_api_keys). Процедура: выпустить новый ключ, переключить инстанс SDK, через время пометить старый revoked. Без grace-периодов с двумя живыми ключами — новый работает до отзыва старого, три шага независимы.
Audit-логи. Финальный слой — журнал вызовов. Таблица btx_api_log пишет на каждый запрос: метод, endpoint, статус-код, response_time_ms, request_id (UUID), retention 90 дней, фильтр по ключу и статусу на app.botix.pro/developers. Журнал закрывает отладку, расследование инцидентов и оптимизацию — если что-то вызывается слишком часто, возможно, пора переехать с polling на webhook. О чём и следующий раздел. Полный контур «ключи + OAuth + аудит» — в разборе multi-tenant: изоляция, OAuth и хранение ключей.
Polling или webhook: критерии выбора для чат-интеграций
Когда интеграции нужно реагировать на события на стороне платформы — новые сообщения, оплаты, обновления контактов — есть две принципиально разные опции. Либо периодически дёргать API и спрашивать «что нового» (polling), либо подписаться на webhook и получать события push'ем. Внешне разница мала, в проде — разная нагрузка, задержка и требования к инфраструктуре.
Polling — запрос «дай всё, что появилось после отметки». В BOTIX это cursor-пагинация: эндпоинты GET /public/v1/chats, GET /public/v1/messages, GET /public/v1/contacts принимают query-параметр cursor и возвращают meta.next_cursor для следующей страницы. Клиент хранит последний cursor, опрашивает периодически, идёт вперёд до пустого next_cursor:
cursor = state.load_cursor(project_id) # None на старте
while True:
resp = client.messages.list(cursor=cursor, per_page=100)
for msg in resp["data"]:
handle(msg)
cursor = resp["meta"]["next_cursor"]
if not cursor:
state.save_cursor(project_id, resp["meta"]["next_cursor"])
time.sleep(30) # период опросаПростота — главное достоинство: не нужен публичный endpoint, HTTPS-сертификат, обработка повторных доставок. Недостаток — период опроса определяет лаг. Опрос раз в секунду — лаг до 1 с, но 86 400 запросов в сутки на ключ; раз в минуту — лаг до 60 с, для интерактивного бота неприемлемо. И при росте числа проектов один опрашивающий воркер быстро упрётся в rate-limit: X-RateLimit-Limit per minute зависит от тарифа (api_rate_per_minute — от 30 до 2000). На каждом ответе платформа отдаёт X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; на 429 — дополнительно Retry-After. Backoff по этим заголовкам — обязателен, механику разбираем в чек-листе: идемпотентность, backoff, rate-limit.
Webhook — HTTP-запрос, который платформа шлёт на ваш endpoint в момент события. Получатель регистрируется через POST /public/v1/webhooks или в кабинете, выбирает типы событий, отдаёт URL. Задержка — сетевой round-trip плюс обработка на платформе, обычно десятки-сотни миллисекунд. Цена — инфраструктура: публичный endpoint, валидный HTTPS, проверка подписи на каждом запросе, обработка повторных доставок, ответ в пределах нескольких секунд.
Факты доставки в BOTIX (через WebhookDispatcher, очередь btx_api_webhook_deliveries, cron раз в минуту):
- Подпись — HMAC-SHA256 от тела запроса, заголовок
X-Botix-Signatureв формеsha256=…, секрет хранится у обеих сторон. Рядом идутX-Botix-EventиX-Botix-Request-Id. - Retry-расписание: 30 секунд / 5 минут / 30 минут / 2 часа / 6 часов, максимум 5 попыток. После — статус
failed, событие видно в delivery dashboard. - События, эмитируемые из public-контроллеров:
contact.created,contact.updated,contact.deleted,contact.tagged,message.sent,scenario.started. Остальные подключаются из bot-движка позже — не рассчитывайте на то, чего нет в списке.
Критично держать проверку подписи на raw body: если распарсить JSON и пересериализовать, порядок ключей и пробелы меняются — HMAC не сойдётся. Это типовая грабля при переходе с polling на webhook.
Когда какой. Алгоритм для типовой чат-интеграции:
- Есть стабильный публичный endpoint и нужна задержка < 5 секунд — webhook, без раздумий.
- Разработка на локальной машине, публичный endpoint поднять сложно — polling через cursor с периодом 30–60 секунд, переход на webhook в момент выхода в прод.
- Интеграция запускается из cron-job раз в час — polling, webhook здесь бессмысленен.
В чистом виде polling в BOTIX — fallback, не основной способ (явно помечено в developers.botix.pro/best-practices). Причина: при росте числа проектов опрашивающий воркер первым упирается в rate-limit, а растянуть период без потери качества нельзя.
Гибрид для дорогих событий. В зрелых интеграциях основной поток — webhook, резервный — periodic reconciliation: раз в час клиент опрашивает API через cursor и сверяет локальное состояние с платформой. Если webhook потерялся или обработчик вернул ошибку — реконсиляция это подберёт. Полезно, когда пропуск события дорогой (не доставили триггер по платёжному статусу). Webhook закрывает 99%, реконсиляция страхует остаток. Здесь же уместен Idempotency-Key на исходящих POST /messages и POST /scenarios/{id}/run — при повторе возвращается тот же результат с заголовком Idempotent-Replayed: 1 (кеш в Redis 24 часа), чтобы реконсиляция не задвоила отправку. Как это ложится в OAuth-конфигурацию — в multi-tenant API: polling vs webhook и OAuth.
Хранение API-ключей: семь антипаттернов и рабочая схема
API-ключ — это пароль в открытом виде: без срока действия по умолчанию, без второго фактора, без попыточного лимита. Кто получил ключ — делает всё, что доступно тарифу. Поэтому компрометация одного ключа — потенциально полный slave-доступ к проекту. И большинство утечек — не изощрённый взлом, а семь повторяющихся антипаттернов:
- Ключ в репозитории.
$key = 'btx_live_XXXX…';в конфиге → коммит → репозиторий стал публичным. Gitleaks-сканеры (GitHub Secret Scanning) поймают паттернbtx_live_…и уведомят владельца — но между публикацией и отзывом проходят часы, за которые базу успевают выгрузить. - Ключ в
.envбез шифрования. Plaintext на диске: RCE, бэкап-копия, забытый.envвнутри Docker-образа в публичном реестре — и все ключи читаются разом..envприемлем как источник переменной, но не как хранилище: ключ должен попадать туда из защищённого источника при деплое. - Один ключ на всю команду. Master-ключ в Slack, за два года ушли пять человек, при увольнении не ротировался. Контрмера — один ключ на исполнителя (сервис/воркер), не на человека.
- Ключ в логах.
error_log(print_r($headers, true))→ в логахAuthorization: Bearer XXXXXXXX…, дальше ELK/Loki/Datadog. Стандартные SDK BOTIX (PHP, Python, Node) маскируют: при сериализации заголовков перед логированиемAuthorizationзаменяется наBearer ***. Ручной код — проговаривать это в код-ревью. - Ключ в URL.
?api_key=…пишется в access.log nginx, логи прокси,Refererпри переходах. Платформа BOTIX категорически не принимает ключ через query-string — толькоAuthorization: Bearer XXXXXXXX…, это закреплено явной проверкой вapp/Middleware/PublicApiAuth.php. - Ключ без ротации. Бессрочный пароль: чем дольше живёт, тем выше шанс, что уже утёк. Гигиена — раз в 6 месяцев планово плюс внепланово по подозрению. Для безболезненности — два состояния (новый работает, старый ещё не отозван), свитч атомарный, отзыв отложенный.
- Ключ с избыточными правами. Сервис делает только
POST /messages, а ключ может всё. Контрмера — те же scopes:messages:sendи ничего больше, тогда при компрометации побочных эффектов нет.
Нормальная схема. Источник правды — secret manager (HashiCorp Vault, Yandex Lockbox, AWS Secrets Manager; в простом случае — pass поверх gpg). Ключ хранится там в зашифрованном виде, доступ по ролям, у каждого доступа — свой audit-лог. При деплое скрипт читает ключ и подставляет в ENV процесса (docker run -e BOTIX_API_KEY=… или Kubernetes Secret). SDK читает ключ из ENV — в коде нет строковой константы, нет конфига с ключом, нет файла; если ENV не установлен, SDK падает на старте с понятной ошибкой. В логах ключ маскируется, в URL не передаётся, на каждый сервис — свой ключ с минимальным scope. Примеры инициализации SDK на PHP/Python/Node из ENV — на developers.botix.pro/best-practices.
Если ключи всё же лежат в вашей БД (например, интегратор хранит по ключу на клиента — паттерн из первого раздела), минимальный приемлемый уровень — шифрование на уровне приложения, AES-256-GCM с ключом из ENV:
const { createCipheriv, randomBytes } = require('crypto');
const KEY = Buffer.from(process.env.WEBHOOK_SECRET_KEY, 'hex'); // 32 байта
function encrypt(text) {
const iv = randomBytes(12);
const c = createCipheriv('aes-256-gcm', KEY, iv);
const enc = Buffer.concat([c.update(text, 'utf8'), c.final()]);
return Buffer.concat([iv, c.getAuthTag(), enc]).toString('base64');
}Зрелый уровень — тот же secret manager: приложение читает секрет в память по запросу и не держит локально. При утечке БД секреты подписи не уходят вместе с остальными данными. Тот же приём применяется к секретам webhook-подписи и к OAuth-токенам, которыми приёмник ходит обратно в API за деталями события. Как это раскрывается в связке с white-label и партнёрским контуром — в хранение ключей, OAuth и white-label.
Как это проверить перед проходом в прод
Единый чек-лист по трём разделам — минута на пункт:
- Ключи хранятся отдельно на каждый
project_idи кэшируются на старте процесса. - Каждая операция в коде явно указывает
project_id— нет глобального «текущего проекта». - Scope каждого ключа сужен до минимально необходимого; сервис-отправитель — только
messages:send. - Ротация возможна без остановки обработчика (
active→revoked, свитч атомарный). - Способ доставки выбран по замеру: задержка < 5 с и живой endpoint → webhook; cron/локалка → polling через cursor.
- Подпись webhook проверяется на raw body; backoff строится по
X-RateLimit-*иRetry-After. .envв.gitignore, в исходниках нет строкbtx_live_…, конфиг читает ключ черезgetenv(), в логахAuthorizationзамаскирован, ключ не уходит в URL.
Пропущен хоть один — архитектура работает только до первого инцидента. Дальше выясняется, что упрощения на старте стоят дороже, чем правильная схема с первого дня.
Попробуйте API BOTIX
Получите ключ, отправьте первый запрос и поймайте webhook — прямо в браузере, без боевого канала.
Попробовать API бесплатноЧитайте также
Multi-tenant, polling vs webhook и OAuth для агентства
Чек-лист интеграции: идемпотентность, backoff, webhook
OAuth, хранение ключей и white-label в партнёрском API
Multi-tenant API: OAuth, scopes и изоляция project_id
Third-party на OAuth: multi-tenant и хранение токенов
