Architecture

Request flow

  1. GitHub sends a webhook POST request to /github.
  2. GitHubSignatureValidationMiddleware validates the request’s HMAC signature and records the result on HttpContext.Items["GitHubSignatureValid"].
  3. GitHubController reads that result, extracts the sender and event details, and hands the payload to GitHubWebhookService.
  4. The service publishes the payload via BrokerProducer. BrokerRouter first reserves capacity for the message: on the active broker connection while it is below its monthly limit, otherwise on the next configured connection with room, which then becomes the active one. A valid signature returns 202 Accepted; an invalid one returns 401 Unauthorized (the payload is still logged, not silently dropped). Either way the response body names the broker connection the payload went through (broker), so a delivery can be traced to a specific broker when several are configured. If every connection is at its limit the payload is not published and the response is 503 Service Unavailable — see API Reference.
  5. The sibling webhooks project consumes the message from the broker. It is the only consumer of forwarded payloads.

Why a persistent broker connection

The original publisher was written in PHP and opened a new broker connection on every incoming request — slow and resource-heavy on shared hosting. This service is a from-scratch C# rewrite specifically to hold one long-lived connection to the broker instead, cutting per-request latency and resource usage. See ADR-0001.

BrokerConnectionManager owns that connection and opens it lazily on first use, reusing it across requests as long as it stays open.

Multiple broker connections and rotation

CloudAMQP’s free LavinMQ tier caps each broker at 2 million messages/month. Rather than pay for a higher tier immediately, this service is configured with multiple broker connections (currently up to 3, defined in a .env file — see Configuration) and rotates between them as each one reaches its monthly limit. See ADR-0002 and ADR-0003.

Rotation is automatic and enforced before publishing. BrokerUsageStore keeps a per-broker, per-month message counter in SQLite; BrokerRouter reserves one unit of it with a single atomic increment-if-below-limit, so concurrent requests cannot push a broker past its limit. When the active connection is full the router walks the BROKER_URL_N order (wrapping around) to the first connection with capacity, switches BrokerConnectionManager to it, and logs the switch. BrokerProducer notices the active index changed and reconnects on the next publish. When no connection has capacity the message is refused with 503 and an error is logged.

Each connection’s limit is BROKER_LIMIT_N from .env when set, otherwise Broker:MonthlyMessageLimit (default 2,000,000). Counters are per calendar month, so exhausted connections come back into rotation when the month rolls over.

The /broker endpoints (bearer-token protected, see API Reference) let the operator:

  • list the configured connections and see which is active,
  • switch the active connection by hand (the router rotates away again if that connection is at its limit),
  • reload the connection list and limits from .env without restarting the process,
  • inspect and prune the usage counters.

This project as a template

WebhooksHandler is a PoC/base implementation, intended as the prototype for future webhook handlers for other projects (GStraccini, Vagas Labeller, Projects Monitor). Whether those will literally redeploy this codebase with different connection strings, or reuse only the architectural pattern, is not yet decided.

Observability

  • /api/health reports API and RabbitMQ connectivity health, polled by UptimeRobot every 5 minutes.
  • Errors are reported to the Projects Monitor system via the Logger configuration section (PmLogger). A LogStream integration and plugging the health endpoint into an infrastructure map are both planned.
  • /stats exposes per-GitHub-sender webhook activity, distinct from broker message usage.

For the full domain vocabulary (Broker, Broker Connection, Active Connection, Operator, Consumer, Sender, Usage), see CONTEXT.md at the repo root.