Webhooks — Architecture
Four distinct ways data moves through this app: async GitHub ingestion through a queue, direct synchronous handlers for every other source, read-only dashboard queries, and internally triggered background jobs. Each is a real, separate mechanism, not a stage of one pipeline — the map below draws all four side by side so it’s clear what’s shared (nginx, PHP-FPM, the database) and what isn’t (only GitHub goes through a queue).
The four flows
1. GitHub webhooks — async, queued
GitHub deliveries never hit this app’s HTTP endpoints directly. A separate service,
WebhooksHandler, validates the HMAC signature and
publishes onto the Broker’s github-exchange, a fanout bound to two independent Queues — one
publish, two consumers, each with its own Worker and failure domain (see
ADR-0008 and Broker Queues for
the full 12-queue map):
github, consumed by theserviceWorker: writes the raw event to thegithublog table (saveGitHubNotificationToDatabase()inSrc/lib/database.php) and atomically upserts itsgithub_statistics_6hbucket in the same code path (see ADR-0004), then composes and sends the outbound email/push Alert. It never touches an entity table.github-database, consumed by thedatabase-serviceWorker: runsReleaseTrackingHandlerfirst, watchingrelease/workflow_runevents across every repository (not just the GStraccini-bot App the rest of this queue is scoped to) — see ADR-0006; this is the persistence layer the futureprojects-monitorproject will read from. Only then, scoped to GStraccini-bot specifically, does it dispatch per event type to either the OOP pipeline (GitHubEventHandler→Library/*Repository.php, forcreate/delete,discussion,issues,pull_request) or the older procedural handlers inSrc/lib/bot.database.php(check_run, comments, installation, etc.), writing into whichever typedgithub_*table the event maps to.
Deduplication is a unique key on (DeliveryId, HookId, TargetId) — duplicate deliveries are caught
and discarded, not retried. If the Broker is unreachable, a circuit breaker (lib/CircuitBreaker.php)
trips and the payload is written to Src/failed/ instead of being lost.
2. Every other webhook source — sync, direct HTTP
AppVeyor, CloudAMQP, DeepSource, GitGuardian, GitHub Status, HealthChecks.io, Oracle OCI,
workflow-webhook releases, SonarCloud, and Wise all post straight to this app’s own endpoints
(POST /<name>). nginx rewrites the request to index.php?handler=<name>, and Src/index.php
require_onces the matching flat, procedural script under Src/handlers/ — no queue, no separate
validator service. The handler saves to its table(s) and sends email/push notifications in the same
request, then returns 202.
3. Dashboard API — sync, read-only
GET /api/v1/... routes go through the same nginx → PHP-FPM runtime as flow 2, but to a dedicated
script under Src/api/v1/ instead of a handler. Every endpoint calls validateApiAccess() (checks
an Authorization: token <...> header) and responds via respondJson(), which flushes the response
to the client via fastcgi_finish_request() before any slower logging calls run — this app never
writes through this path, only reads.
4. Background jobs — internally triggered
Four workers (service, cleanup, database-service, maintenance) share one dispatcher,
runJob() (lib/runner.php), reachable three ways: a plain cron invocation, a long-lived
daemon loop under systemd, or a one-shot POST /api/v1/workers/{name}/run HTTP trigger. Whichever
mode fired it, the same runner function executes — e.g. runCleanupJob() calls cleanupTable() to
sweep expired rows per the retention windows in config/config.json, and pings HealthChecks.io as a
heartbeat.
Related decisions
- ADR-0003 — Compress
github.Payloadstorage - ADR-0004 —
github_statistics_6hbucketed statistics - ADR-0006 — Release + workflow tracking for
projects-monitor - Full ADR log for every recorded decision, not just the ones referenced above.
- Production VM Setup for how this all actually gets deployed and run.