Разработчик трижды собирал одну и ту же систему: в трёх компаниях, для трёх провайдеров. Правда о клиентах живёт в чужой базе: пользователи — в identity provider, подписки — в Stripe, отказы писем — в почтовом сервисе. Продукту нужна локальная копия, поэтому он подписывался на webhooks.
Сначала казалось, что достаточно одного endpoint: принимаешь JSON, обновляешь строку. Потом добавилась проверка подписи, таблица дедупликации (доставка «at-least-once»), буфер для событий, которые приходят не в том порядке, импорт начального состояния и ночной cron для сверки со списками провайдера. Этот cron — письменное признание: я не доверяю своей копии и не знаю, когда она врёт, поэтому каждую ночь пересобираю её с нуля.
Дрейф нашёлся через тикет саппорта: клиент отменил подписку месяцы назад, а в базе всё ещё значился active. Событие customer.subscription.deleted где-то потерялось, и никто не мог это заметить. Дашборд Stripe показывал, что доставку ретраили и бросили, а логи не фиксируют запрос, который не пришёл.
Он понял: на самом деле он пытается восстановить упорядоченный лог. Лог существует у провайдера — иначе как отрисуются их дашборды и replay-инструменты? Провайдер режет лог на отдельные HTTP POST и шлёт по каналу без гарантий порядка и доставки. Потребитель собирает пазл вслепую; если кусок потерялся, никто об этом не объявит.
Термин webhook придумал Джефф Линдси в 2007 году, и сначала всё было честно: GitHub запускал CI, оплата слала чек. Но как способ передать набор данных webhooks ужасны. Это локальный оптимум: вокруг выросла индустрия обходных путей — Svix, Hookdeck, AWS EventBridge+SQS+Lambda, коннекторы Fivetran и Airbyte, которые пересобирают CDC из webhooks и опросов API. Даже stripe listen — туннель для доставки на localhost — работает вокруг направления доставки собственного примитива.
Некоторые провайдеры оглянулись: Stripe хранит 30 дней событий и рекомендует сверяться с /v1/events; WorkOS советует свой Events API вместо webhooks. Но общего контракта нет.
Предложение: пусть провайдер отдаёт на каждый набор данных упорядоченный лог с курсорами. GET /feed/customers?cursor=... без курсора читает всё с начала — это и есть bootstrap; с курсором — продолжает. События несут полное текущее состояние объекта, так что повторное применение даёт тот же результат. Удаления становятся tombstone-событиями в логе. Подключение инициирует потребитель, так что не нужны ни endpoint, ни подпись, ни туннель. В конце лога провайдер может отдавать count и checksum — по ним проверяешь, что реплика верна, без ночного cron.
Никто так не делает. Автор оформил идею как протокол SCROLL (Synchronized Change Replication Over Line Logs) на welidev.github.io/scroll. Шим сможет собрать такой фид из существующих webhooks и list API. Он просит прочитать черновик и сказать, где он ломается.