voyvodka/webhook-engine

By voyvodka

Updated 23 days ago

Self-hosted webhook delivery platform — PostgreSQL queue, retry, HMAC signing, React dashboard.

Image
Networking
Developer tools
0

1.1K

voyvodka/webhook-engine repository overview

WebhookEngine

webhook.sametozkan.com.tr · Docker Hub · NuGet

Self-hosted webhook delivery platform with reliable at-least-once delivery, exponential backoff retries, per-endpoint circuit breakers, and a real-time dashboard.

Features

  • Reliable delivery -- PostgreSQL-backed queue with SELECT ... FOR UPDATE SKIP LOCKED, no Redis/RabbitMQ needed
  • At-least-once semantics -- messages are never lost; stale lock recovery handles worker crashes
  • Exponential backoff -- 7 retry attempts (5s, 30s, 2m, 15m, 1h, 6h, 24h)
  • Circuit breaker -- per-endpoint, auto-opens after 5 consecutive failures, 5-minute cooldown
  • HMAC-SHA256 signing -- Standard Webhooks spec (webhook-id, webhook-timestamp, webhook-signature)
  • Idempotency -- optional idempotencyKey prevents duplicate deliveries; per-event-type window override
  • Payload transformation -- per-endpoint JMESPath expression reshapes the body before signing; fail-open with timeout and output-size guards (ADR-003); live editor in the dashboard
  • SSRF guard -- endpoint URLs rejected at create / update and at connect time when they resolve to RFC1918 / loopback / cloud-metadata ranges; SocketsHttpHandler.ConnectCallback pins the resolved IP for the lifetime of each request (DNS-rebinding defense)
  • Per-endpoint IP allowlist -- opt-in CIDR positive-list; deliveries are gated against the resolved IP at attempt time
  • Per-resource overrides -- per-application rate limit and retention windows; per-event-type idempotency window
  • Endpoint test webhook -- send a fully-signed test delivery from the dashboard without enqueueing a real message; signed-request preview included
  • Append-only audit log -- admin actions across applications, endpoints, event types, replay, retry, and rotate-key are recorded with before/after snapshots
  • Real-time dashboard -- React SPA with live delivery feed and circuit-state changes via SignalR; data layer on TanStack Query
  • Single process -- API + background workers + dashboard served from one ASP.NET Core host
  • Data retention -- automatic cleanup (delivered: 30 days, dead-letter: 90 days; per-app overrides supported)
  • Embeddable customer portal -- ship a <EndpointManager /> React component into your own settings UI; per-application HS256 JWT auth minted by your backend, RFC 6454 dynamic CORS, capability scoping (endpoints:read|write|test, attempts:read); operator-managed signing key + allowed origins from the dashboard

Tech Stack

LayerTechnology
BackendC# / .NET 10, ASP.NET Core, Entity Framework Core
DatabasePostgreSQL 17+
FrontendReact 19, TypeScript 6, Vite 8, Tailwind CSS 4, TanStack Query 5, Recharts 3, Lucide React
Real-timeSignalR
TestingxUnit, FluentAssertions, NSubstitute, Testcontainers
LoggingSerilog (structured JSON)
ValidationFluentValidation
ObservabilityOpenTelemetry + Prometheus metrics exporter
DeploymentDocker Compose

Quick Start

Important

**The published image requires a PostgreSQL database** — it cannot be launched standalone via Docker Hub's "Run" button. The container's first action on startup is `Database.MigrateAsync()`; with no database reachable it exits within seconds. Use one of the two options below.

The bundled compose file starts PostgreSQL alongside the engine, with sensible defaults:

git clone https://github.com/voyvodka/webhook-engine.git
cd webhook-engine
docker compose -f docker/docker-compose.yml up -d

The app starts on http://localhost:5100. Dashboard login: [email protected] / changeme.

Manual docker run against an existing PostgreSQL

If you already operate a PostgreSQL instance, run the image directly and point it at your database:

docker run -d --name webhook-engine \
  -p 8080:8080 \
  -e ConnectionStrings__Default="Host=your-postgres;Port=5432;Database=webhookengine;Username=postgres;Password=secret" \
  -e Dashboard__AdminEmail="[email protected]" \
  -e Dashboard__AdminPassword="StrongPassword123!" \
  voyvodka/webhook-engine:latest

The container listens on port 8080 internally; map it to whatever host port suits your environment. See docs/SELF-HOSTING.md for the full configuration reference (rate limits, retention, retry policy, signing secret rotation, etc.).

Local Development

Prerequisites: .NET 10 SDK, PostgreSQL 17+, Node.js 20+, Bun 1.2+

  1. Start PostgreSQL (or use the dev compose file):
docker compose -f docker/docker-compose.dev.yml up -d
  1. Configure connection string in src/WebhookEngine.API/appsettings.json:
{
  "ConnectionStrings": {
    "Default": "Host=localhost;Port=5432;Database=webhookengine;Username=webhookengine;Password=webhookengine"
  }
}
  1. Run the backend (migrations auto-apply on startup):
dotnet run --project src/WebhookEngine.API

The API starts on http://localhost:5128.

  1. Run the dashboard (optional, for frontend development):
cd src/dashboard
bun install
bun run dev

Dashboard dev server runs on http://localhost:5173 with API proxy to localhost:5128.

Documentation

Architecture

                    +---------------------------+
                    |    ASP.NET Core Host       |
                    |                           |
   HTTP requests -> | Controllers (REST API)    |
                    | Middleware (auth, logging) |
                    | Static files (React SPA)  |
                    | SignalR Hub               |
                    |                           |
                    | Background Workers:       |
                    |  - DeliveryWorker         |
                    |  - RetryScheduler         |
                    |  - CircuitBreakerWorker   |
                    |  - StaleLockRecovery      |
                    |  - RetentionCleanup       |
                    +------------+--------------+
                                 |
                                 v
                    +---------------------------+
                    |     PostgreSQL 17+         |
                    |  - Data storage            |
                    |  - Job queue (SKIP LOCKED) |
                    +---------------------------+
Solution Structure
src/
  WebhookEngine.Core/            # Domain: entities, enums, interfaces, options
  WebhookEngine.Infrastructure/   # EF Core, PostgreSQL queue, repositories, services
  WebhookEngine.Worker/           # Background services (delivery, retry, circuit breaker)
  WebhookEngine.API/              # ASP.NET Core host, controllers, middleware
  WebhookEngine.Sdk/              # .NET client SDK
  dashboard/                      # React SPA (Vite + TypeScript)
tests/
  WebhookEngine.Core.Tests/
  WebhookEngine.Infrastructure.Tests/
  WebhookEngine.API.Tests/
  WebhookEngine.Worker.Tests/

API Overview

Base URL: /api/v1/

Authentication
  • API key (for programmatic access): Authorization: Bearer whe_{appId}_{random}
  • Cookie auth (for dashboard): POST /api/v1/auth/login
Endpoints
MethodPathAuthDescription
GET/health or /api/v1/healthNoneHealth check
GET/health/liveNoneLiveness probe (process up)
GET/health/readyNoneReadiness probe (DI + DB ready, migrations applied)
POST/api/v1/auth/loginNoneDashboard login
POST/api/v1/auth/logoutCookieDashboard logout
GET/api/v1/auth/meCookieCurrent user
GET/api/v1/applicationsCookieList applications
POST/api/v1/applicationsCookieCreate application
GET/api/v1/applications/{id}CookieGet application
PUT/api/v1/applications/{id}CookieUpdate application (incl. retention / rate-limit overrides)
DELETE/api/v1/applications/{id}CookieDelete application (cascades to messages)
POST/api/v1/applications/{id}/rotate-keyCookieRotate API key
POST/api/v1/applications/{id}/rotate-secretCookieRotate signing secret
GET/api/v1/event-typesAPI keyList event types
POST/api/v1/event-typesAPI keyCreate event type
GET/api/v1/endpointsAPI keyList endpoints
POST/api/v1/endpointsAPI keyCreate endpoint (URL DNS-validated)
PUT/api/v1/endpoints/{id}API keyUpdate endpoint
DELETE/api/v1/endpoints/{id}API keyDelete endpoint (cascades to messages)
POST/api/v1/endpoints/{id}/disableAPI keyDisable endpoint
POST/api/v1/endpoints/{id}/enableAPI keyEnable endpoint
POST/api/v1/messagesAPI keySend message
POST/api/v1/messages/batchAPI keyBatch send messages
POST/api/v1/messages/replayAPI keyReplay historical messages
GET/api/v1/messagesAPI keyList messages
GET/api/v1/messages/{id}API keyGet message
GET/api/v1/messages/{id}/attemptsAPI keyList attempts
POST/api/v1/messages/{id}/retryAPI keyRetry message
GET/api/v1/dashboard/overviewCookieDashboard stats
GET/api/v1/dashboard/timelineCookieDelivery chart data
GET/api/v1/dashboard/event-typesCookieList event types (cross-app)
POST/api/v1/dashboard/event-typesCookieCreate event type
PUT/api/v1/dashboard/event-types/{id}CookieUpdate event type
DELETE/api/v1/dashboard/event-types/{id}CookieArchive event type
POST/api/v1/dashboard/endpoints/{id}/testCookieSend a customizable, fully-signed test webhook
POST/api/v1/dashboard/transform/validateCookieValidate a JMESPath expression against a sample payload
GET/api/v1/dashboard/auditCookieList audit log entries (filterable)
Send a Message
curl -X POST http://localhost:5100/api/v1/messages \
  -H "Authorization: Bearer whe_abc123_your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "order.created",
    "payload": {"orderId": 42, "amount": 99.99},
    "idempotencyKey": "order-42"
  }'

Response:

{
  "data": {
    "messageIds": ["msg_abc123..."],
    "endpointCount": 2,
    "eventType": "order.created"
  },
  "meta": { "requestId": "req_..." }
}
.NET SDK
using WebhookEngine.Sdk;

using var client = new WebhookEngineClient("whe_abc_your-api-key", "http://localhost:5100");

await client.Messages.SendAsync(new SendMessageRequest
{
    EventType = "order.created",
    Payload = new { orderId = 42, amount = 99.99 },
    IdempotencyKey = "order-42"
});

Webhook Signature Verification

WebhookEngine signs every delivery with HMAC-SHA256 following the Standard Webhooks spec. Receivers should verify signatures like this:

# Python example
import hmac, hashlib, base64

def verify_webhook(body: bytes, secret: str, headers: dict) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signature = headers["webhook-signature"]

    payload = f"{msg_id}.{timestamp}.{body.decode()}"
    secret_bytes = base64.b64decode(secret)
    expected = hmac.new(secret_bytes, payload.encode(), hashlib.sha256).digest()
    expected_sig = f"v1,{base64.b64encode(expected).decode()}"

    return hmac.compare_digest(signature, expected_sig)

Configuration

All configuration is via appsettings.json or environment variables (double-underscore notation):

SettingDefaultDescription
ConnectionStrings__Default--PostgreSQL connection string
WebhookEngine__Delivery__TimeoutSeconds30HTTP delivery timeout
WebhookEngine__Delivery__BatchSize10Messages dequeued per batch
WebhookEngine__Delivery__PollIntervalMs1000Queue poll interval (empty queue)
WebhookEngine__Delivery__StaleLockMinutes5Stale lock recovery threshold
WebhookEngine__RetryPolicy__MaxRetries7Max delivery attempts
WebhookEngine__RetryPolicy__BackoffSchedule[5,30,120,900,3600,21600,86400]Backoff in seconds
WebhookEngine__CircuitBreaker__FailureThreshold5Failures to open circuit
WebhookEngine__CircuitBreaker__CooldownMinutes5Cooldown before half-open
WebhookEngine__DashboardAuth__AdminEmail[email protected]Initial admin email
WebhookEngine__DashboardAuth__AdminPasswordchangemeInitial admin password
WebhookEngine__Retention__DeliveredRetentionDays30Days to keep delivered messages
WebhookEngine__Retention__DeadLetterRetentionDays90Days to keep dead-letter messages

Build & Test

# Build
dotnet build WebhookEngine.sln

# Run all tests
dotnet test WebhookEngine.sln

# Run specific test project
dotnet test tests/WebhookEngine.Core.Tests

# Run tests matching a pattern
dotnet test --filter "DisplayName~HmacSigning"

# Dashboard build
cd src/dashboard && bun install && bun run build

Docker

# Production
docker compose -f docker/docker-compose.yml up -d

# Stop production services
docker compose -f docker/docker-compose.yml down

# Reset production data (removes PostgreSQL volume)
docker compose -f docker/docker-compose.yml down -v

# Development (starts PostgreSQL only, run backend separately)
docker compose -f docker/docker-compose.dev.yml up -d
dotnet run --project src/WebhookEngine.API

docker/docker-compose.yml uses a persistent PostgreSQL volume (pgdata), so old applications/endpoints remain after restart unless you run down -v.

Prometheus Metrics

WebhookEngine exposes Prometheus metrics at GET /metrics. No authentication required.

curl http://localhost:5100/metrics
Custom Metrics
MetricTypeDescription
webhookengine_messages_enqueuedCounterTotal messages enqueued
webhookengine_deliveries_totalCounterTotal delivery attempts
webhookengine_deliveries_successCounterSuccessful deliveries
webhookengine_deliveries_failedCounterFailed deliveries
webhookengine_deadletter_totalCounterMessages moved to dead letter
webhookengine_retries_scheduledCounterRetry attempts scheduled
webhookengine_circuit_openedCounterCircuit breaker open events
webhookengine_circuit_closedCounterCircuit breaker close events
webhookengine_stalelock_recoveredCounterStale locks recovered
webhookengine_delivery_durationHistogramDelivery duration (ms)
webhookengine_queue_depthUpDownCounterApproximate queue depth
Built-in Metrics

ASP.NET Core request metrics and .NET runtime metrics (GC, thread pool, etc.) are also included automatically.

Prometheus Scrape Config
# prometheus.yml
scrape_configs:
  - job_name: webhookengine
    scrape_interval: 15s
    static_configs:
      - targets: ["localhost:5100"]

Message Lifecycle

  Send API call
       |
       v
  [Pending] --dequeue--> [Sending] --success--> [Delivered]
       ^                     |
       |                     | failure
       |                     v
       +--retry-schedule-- [Failed] --max-retries--> [DeadLetter]
  • Pending: Queued for delivery
  • Sending: Locked by a worker, in-flight
  • Delivered: Successfully delivered (HTTP 2xx)
  • Failed: Delivery failed, scheduled for retry
  • DeadLetter: All retry attempts exhausted

License

MIT

Tag summary

Content type

Image

Digest

sha256:202877be4

Size

58.2 MB

Last updated

23 days ago

docker pull voyvodka/webhook-engine