Inquir Compute · self-hosted ingress

Self-hosted ingress для вебхуков на вашем домене

Направьте Stripe, GitHub или Slack на URL вашего hostname — DNS и TLS в вашем аккаунте. Проверяйте подписи по сырому телу, подтверждайте в лимите провайдера и продолжайте тяжёлую работу в serverless-функциях или пайплайнах на Inquir.

Обновлено: 2026-06-28

Суть ответа

Self-hosted ingress для вебхуков на вашем домене. Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой хендлер с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.

Когда подходит и когда нет

  • Нужен стабильный HTTPS на hostname под вашим контролем (custom domain + DNS) и serverless-хендлеры на Inquir.
  • Хочется развести трафик вебхуков и пользовательские API одного продукта.

На что обратить внимание

  • Один эндпоинт на VPS смешивает деплой с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.
  • Монолитный маршрут часто смешивает cookie-пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.

Зачем self-hosted URL для вебхуков

Команды держат вебхуки на своём сервере в первую очередь потому, что провайдеры добавляют callback URL в белый список: стабильный hostname под вашим контролем — hooks.yourcompany.com вместо чужого поддомена — проще объяснить службе безопасности и вписать в правила firewall.

Встроенная проверка прямо в основном API всё ещё смешивает масштабирование и режимы auth. Отдельный ingress на вашем домене держит машинные ключи отдельно от пользовательских сессий.

Почему VPS или монолитный эндпоинт хрупки

Один эндпоинт на VPS смешивает деплой с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.

Монолитный маршрут часто смешивает cookie-пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.

Self-hosted ingress, управляемые serverless-хендлеры

Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой хендлер с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.

Привяжите свой домен к именованному API через TXT-запись для верификации и CNAME или A; TLS выпускается по запросу. Сырые тела для HMAC, трассировки и передача в пайплайны — в одном рабочем пространстве рядом с HTTP API, без SSH на сервер ради просмотра логов вебхуков.

Возможности обработки вебхуков

Изолированные маршруты вебхуков

Разводите пути провайдеров по небольшим хендлерам с узкими правами.

Трассировка выполнения

Смотрите тела (при необходимости сокращённые) и тайминги, когда провайдер ставит доставку на паузу.

Передача в пайплайны

Отвечайте 200 быстро, а тяжёлую обработку продолжайте в пайплайне или задаче, когда окна HTTP не хватает.

Как запустить вебхуки на своём домене

Привяжите custom domain к маршрутам шлюза, проверяйте подписи в serverless-хендлерах, подтверждайте в лимите провайдера, пишите идемпотентно.

1

Настроить DNS на шлюз

Custom domain и TLS — провайдеры бьют в hooks.yourcompany.com, а не в чужой поддомен.

2

Проверить и быстро ACK

Подпись по сырому телу, ключ идемпотентности, 200 в окне провайдера.

3

Продолжить в пайплайнах

Тяжёлую обработку — через global.durable.startNew(), чтобы повторы не дублировали эффекты.

Паттерны провайдеров на вашем домене

Шлюз на вашем hostname отдаёт событие в духе AWS API Gateway: body строкой, заголовки как пришли. По одной функции на провайдера — верификация остаётся компактной.

webhooks/stripe.mjs
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function handler(event) {
  const rawBody = event.body ?? '';
  const sig = event.headers['stripe-signature'] ?? '';
  let evt;
  try {
    evt = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return { statusCode: 400, body: `Webhook Error: ${err.message}` };
  }
  const isNew = await db.upsertWebhookEvent(evt.id, evt.type);
  if (!isNew) return { statusCode: 200, body: 'duplicate' };
  await global.durable.startNew('stripe-fulfillment', undefined, { eventId: evt.id, type: evt.type, data: evt.data.object });
  return { statusCode: 200, body: 'accepted' };
}
webhooks/github.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function handler(event) {
  const body = event.body ?? '';
  const sigHeader = (event.headers['x-hub-signature-256'] ?? '').replace('sha256=', '');
  const expected = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(body).digest('hex');
  if (sigHeader.length !== expected.length ||
      !timingSafeEqual(Buffer.from(sigHeader, 'hex'), Buffer.from(expected, 'hex'))) {
    return { statusCode: 401, body: 'invalid signature' };
  }
  const eventType = event.headers['x-github-event'];
  const payload = JSON.parse(body);
  if (eventType === 'push') {
    await global.durable.startNew('index-repo', undefined, { repo: payload.repository.full_name, sha: payload.after });
  }
  return { statusCode: 200, body: 'accepted' };
}
webhooks/slack.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function handler(event) {
  const body = event.body ?? '';
  const timestamp = event.headers['x-slack-request-timestamp'] ?? '';
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return { statusCode: 400, body: 'stale request' };
  }
  const sigBase = `v0:${timestamp}:${body}`;
  const expected = 'v0=' + createHmac('sha256', process.env.SLACK_SIGNING_SECRET).update(sigBase).digest('hex');
  const received = event.headers['x-slack-signature'] ?? '';
  if (expected.length !== received.length ||
      !timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return { statusCode: 401, body: 'invalid signature' };
  }
  const params = Object.fromEntries(new URLSearchParams(body));
  await global.durable.startNew('slack-command', undefined, {
    command: params.command,
    userId: params.user_id,
    channelId: params.channel_id,
    text: params.text,
    responseUrl: params.response_url,
  });
  return { statusCode: 200, body: '' };
}

Когда self-hosted ingress на Inquir уместен

Когда это уместно

  • Нужен стабильный HTTPS на hostname под вашим контролем (custom domain + DNS) и serverless-хендлеры на Inquir.
  • Хочется развести трафик вебхуков и пользовательские API одного продукта.

Когда лучше выбрать другое

  • Вебхуки уходят только в чужую iPaaS без своей среды выполнения.

Вопросы и ответы

Что делать с медленным downstream?

Подтвердите вебхук быстро, а работу продолжите в serverless-пайплайне или фоновой задаче — тогда таймауты провайдера не блокируют важные побочные эффекты.

Можно ли разводить маршруты по клиентам?

Да. Паттерны мультитенантной маршрутизации позволяют сегментировать пути или хосты — подробности на отдельной странице фичи.

Как защититься от повторных доставок?

Сочетайте проверку подписи, метки времени провайдера, где они поддерживаются, и идемпотентные записи по идентификатору события провайдера.