Architecture
Request flow
- GitHub sends a webhook
POSTrequest to/github. GitHubSignatureValidationMiddlewarevalidates the request’s HMAC signature and records the result onHttpContext.Items["GitHubSignatureValid"].GitHubControllerreads that result, extracts the sender and event details, and hands the payload toGitHubWebhookService.- The service publishes the payload via
BrokerProducer.BrokerRouterfirst 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 returns202 Accepted; an invalid one returns401 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 is503 Service Unavailable— see API Reference. - The sibling
webhooksproject 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
.envwithout 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/healthreports API and RabbitMQ connectivity health, polled by UptimeRobot every 5 minutes.- Errors are reported to the Projects Monitor system via the
Loggerconfiguration section (PmLogger). A LogStream integration and plugging the health endpoint into an infrastructure map are both planned. /statsexposes 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.