webhook-engine

releasev0.3.0

Self-hosted webhook delivery engine with retry, circuit breaker, HMAC signing, rate limiting, and real-time React dashboard. .NET 10 + PostgreSQL. Docker one-liner deploy.

0stars
0forks
0watchers
1open issues
owner: voyvodkastatus: CORElanguage: C#branch: mainlicense: MIT Licenseupdated: 6/15/2026, 12:36:42 PMlast push: 6/16/2026, 4:02:31 PM
aspnet-corecircuit-breakerdashboarddockerdotnethmacpostgresqlreactreal-timeretryself-hostedsignalrtypescriptwebhookwebhook-deliverywebhooks

README Snapshot

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

Layer Technology
Backend C# / .NET 10, ASP.NET Core, Entity Framework Core
Database PostgreSQL 17+
Frontend React 19, TypeScript 6, Vite 8, Tailwind CSS 4, TanStack Query 5, Recharts 3, Lucide React
Real-time SignalR
Testing xUnit, FluentAssertions, NSubstitute, Testcontainers
Logging Serilog (structured JSON)
Validation FluentValidation
Observability OpenTelemetry + Prometheus metrics exporter
Deployment Docker 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.

Docker Compose (recommended)

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

Method Path Auth Description
GET /health or /api/v1/health None Health check
GET /health/live None Liveness probe (process up)
GET /health/ready None Readiness probe (DI + DB ready, migrations applied)
POST /api/v1/auth/login None Dashboard login
POST /api/v1/auth/logout Cookie Dashboard logout
GET /api/v1/auth/me Cookie Current user
GET /api/v1/applications Cookie List applications
POST /api/v1/applications Cookie Create application
GET /api/v1/applications/{id} Cookie Get application
PUT /api/v1/applications/{id} Cookie Update application (incl. retention / rate-limit overrides)
DELETE /api/v1/applications/{id} Cookie Delete application (cascades to messages)
POST /api/v1/applications/{id}/rotate-key Cookie Rotate API key
POST /api/v1/applications/{id}/rotate-secret Cookie Rotate signing secret
GET /api/v1/event-types API key List event types
POST /api/v1/event-types API key Create event type
GET /api/v1/endpoints API key List endpoints
POST /api/v1/endpoints API key Create endpoint (URL DNS-validated)
PUT /api/v1/endpoints/{id} API key Update endpoint
DELETE /api/v1/endpoints/{id} API key Delete endpoint (cascades to messages)
POST /api/v1/endpoints/{id}/disable API key Disable endpoint
POST /api/v1/endpoints/{id}/enable API key Enable endpoint
POST /api/v1/messages API key Send message
POST /api/v1/messages/batch API key Batch send messages
POST /api/v1/messages/replay API key Replay historical messages
GET /api/v1/messages API key List messages
GET /api/v1/messages/{id} API key Get message
GET /api/v1/messages/{id}/attempts API key List attempts
POST /api/v1/messages/{id}/retry API key Retry message
GET /api/v1/dashboard/overview Cookie Dashboard stats
GET /api/v1/dashboard/timeline Cookie Delivery chart data
GET /api/v1/dashboard/event-types Cookie List event types (cross-app)
POST /api/v1/dashboard/event-types Cookie Create event type
PUT /api/v1/dashboard/event-types/{id} Cookie Update event type
DELETE /api/v1/dashboard/event-types/{id} Cookie Archive event type
POST /api/v1/dashboard/endpoints/{id}/test Cookie Send a customizable, fully-signed test webhook
POST /api/v1/dashboard/transform/validate Cookie Validate a JMESPath expression against a sample payload
GET /api/v1/dashboard/audit Cookie List 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 Send

Changelog

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project follows Semantic Versioning.

[Unreleased]

Added

Changed

Fixed

Removed

Security

[0.3.0] - 2026-06-08

Added

  • WebhookEngine.Sdk gains EndpointClient.TestAsync(endpointId, request) — the one live /api/v1/* route with no SDK coverage. Sends a one-off signed test webhook and returns the live response plus the exact signed request (new models TestEndpointRequest, EndpointTestResult, EndpointTestRequestPreview).
  • SDK request models gain the fields the API already accepts: CreateEventTypeRequest/UpdateEventTypeRequest.IdempotencyWindowMinutes, and CreateEndpointRequest/UpdateEndpointRequest.{AllowedIps, TransformExpression, TransformEnabled}. Previously SDK callers could not set per-event-type idempotency windows, per-endpoint IP allowlists, or payload-transform expressions at all.
  • First test coverage for WebhookEngine.Sdk — a new WebhookEngine.Sdk.Tests project (32 cases). Covers WebhookVerifier constant-time HMAC verification across tolerance, secret-encoding (whsec_ vs base64), multi-signature, tamper, and missing-field cases, plus a stub-HttpMessageHandler contract suite that deserializes real API envelopes through the client (exercising the SDK's own camelCase options) so response-DTO drift fails CI.

Changed

  • SDK response models realigned with the engine DTOs (fixes silent field drops). EndpointResponse gains AllowedIps, TransformExpression, TransformEnabled, TransformValidatedAt, and EventTypeResponse gains IdempotencyWindowMinutes — all were sent on the wire but silently dropped on deserialization. EndpointResponse.CustomHeadersJson / MetadataJson and MessageAttemptResponse.RequestHeadersJson change from Dictionary<string,string>? to JsonElement / JsonElement? to match the API, which serialises them as JSON objects; the old dictionary type silently dropped the entire field for any non-string value. Breaking for SDK consumers that read those three properties as dictionaries — the next SDK version should bump accordingly.
  • The concurrency-critical paths now have real regression coverage, and the worker unit tests no longer re-implement production logic. New Testcontainers (real PostgreSQL) tests cover the idempotency UNIQUE race (F7 — N concurrent inserts of the same key yield exactly one row, losers get a 23505), the FOR UPDATE SKIP LOCKED dequeue (K concurrent workers never double-claim a message), and the Mark*Async CAS lock-stolen / wrong-status guard (F2). DeliveryWorker's retry-backoff and header helpers are now internal and exercised directly (the previous tests copied the logic inline, so a worker regression would not have failed them), and RetryScheduler eligibility is now driven through the real requeue SQL. Each race test was mutation-verified (it goes red when the underlying guard is reverted). No runtime behavior change — CalculateNextRetryAt was only parameterized for testability.

Fixed

  • @webhookengine/endpoint-manager client realigned with the live engine. The published 0.1.0 portal client drifted from the engine contract and the drift was invisible in CI because the package's vitest suite and the samples/portal-host mock both reproduced an idealized shape, never the engine's real DTOs. Three concrete defects: updateEndpoint() issued PUT while the engine route is [HttpPatch] (every endpoint update failed against a real engine — the route switched to PATCH in v0.2.1 but the client was never updated); the client read a non-existent isActive boolean instead of the engine's status string, so the status badge always rendered "Disabled"; and it read a non-existent customHeaders map instead of the names-only customHeaderNames, which started the editor empty and sent customHeaders: {} on every save — silently wiping an endpoint's custom headers even when only the URL changed. Types now mirror the engine (status: PortalEndpointStatus, customHeaderNames: string[]), the editor preserves stored headers unless the operator explicitly enters new ones, and a contract test now asserts the client speaks PATCH and deserializes the real response shape so this class of drift cannot ship silently again.
  • Dependabot lockfile-sync commit can now re-trigger CI. sync-bun-lock.yml pushed the regenerated bun.lock with GITHUB_TOKEN, and GitHub deliberately does not let GITHUB_TOKEN pushes trigger new workflow runs (recursion guard) — so the sync commit became the PR's HEAD with no CI run on it, leaving the PR blocked on required status checks until a manual close/reopen (as happened on #113). The push now uses a short-lived GitHub App installation token (actions/create-github-app-token, gated on the optional vars.LOCK_BOT_APP_ID) so the lockfile commit re-triggers the required checks automatically; it falls back to GITHUB_TOKEN (manual re-trigger) when the App is not configured. One-time App setup is documented in docs/RELEASE.md §1.
  • Documentation accuracy pass. README: the manual docker run example used ConnectionStrings__WebhookDb (the engine reads ConnectionStrings__Default), and the general curl / SDK / Prometheus examples used the local-dev port 5128 while the Docker Quick Start is 5100 — so a Docker user copy-pasting them hit the wrong target; the examples now use 5100 (matching Quick Start and the SDK's default base URL) and the test count is no longer a hard-coded number that drifts. docs/PRD.md: payload transformation removed from the MVP non-goals (it shipped via ADR-003). docs/ROADMAP.md: synced to v0.2.2 and the @webhookengine/endpoint-manager package marked released (portal-v0.1.0). docs/ARCHITECTURE.md: the §2 controller list now includes the AuditLogs / Portal / DashboardPortal controllers and the duplicate ### 3.5 heading is fixed (HTTP Delivery Service is ### 3.8). AGENTS.md: frontend stack line corrected to TypeScript 6 / Vite 8.
  • Dependabot lockfile auto-sync now covers every workspace member. sync-bun-lock.yml's pull_request_target trigger only watched src/dashboard/package.json, so a Dependabot bump in packages/endpoint-manager/package.json never regenerated the workspace-root bun.lock and the PR died on CI's bun install --frozen-lockfile (hit on #124 and #129). The trigger now also matches packages/*/package.json, so any current or future packages/* workspace member auto-syncs the lockfile.

Security

  • Repository security baseline hardened. Added a root SECURITY.md (supported-versions policy + GitHub private-vulnerability-reporting disclosure flow), which also resolves the dangling /SECURITY.md reference in CODEOWNERS. Added explicit least-privilege permissions: contents: read blocks to ci.yml and release.yml — previously the two highest-privilege workflows ran with the broad default token (Docker Hub / NuGet auth uses repository secrets, not GITHUB_TOKEN, so no write scope is needed; id-token: write is deliberately omitted while provenance is disabled). Extended Dependabot to the previously-uncovered /packages/endpoint-manager npm workspace (vite, tailwindcss, tsup, vitest, typescript, … now tracked). Added timeout-minutes to the CI Backend/Frontend/Docker jobs so a hung Testcontainers run can't consume the 6-hour default.
  • Secret-scanning false-positive suppression. Added .github/secret_scanning.yml with paths-ignore for docs/**, samples/**, tests/**, **/__tests__/**, and **/*.md. WebhookEngine's Standard Webhooks whsec_ signing-secret prefix collides with Stripe's webhook-secret pattern, so GitHub flagged fabricated whsec_… doc examples and test vectors as "Stripe Webhook Signing Secret" alerts. Those paths never hold live credentials; src/** stays scanned. (The two existing alerts were resolved as false-positive.)

[0.2.2] - 2026-05-29

Maintenance patch: full dependency refresh across NuGet, npm, Docker base images, and GitHub Actions — plus the CI Bun/lockfile alignment fix. No user-visible behaviour changes, no breaking API changes; the v1 route prefix and Standard Webhooks signature surface are unchanged.

Security

  • Docker base image digest bumps. dotnet/aspnet (60eb0311e37a82), dotnet/sdk (8a90a47dc8430e), and oven/bun (4de47535acc90a) all refreshed to pick up the latest patched Alpine layers.

Changed

  • NuGet runtime dependency refresh. Microsoft.AspNetCore.OpenApi, Microsoft.EntityFrameworkCore (+ .Relational, .Design), Microsoft.Extensions.Http, .Hosting.Abstractions, and .DependencyInjection all bumped 10.0.7 → 10.0.8; Scalar.AspNetCore 2.14.11 → 2.14.14. Test-only: FluentAssertions 8.9.0 → 8.10.0, coverlet.collector 10.0.0 → 10.0.1, Microsoft.AspNetCore.Mvc.Testing / EntityFrameworkCore.InMemory 10.0.7 → 10.0.8, Testcontainers.PostgreSql 4.11.0 → 4.12.0.
  • npm / frontend dependency refresh. lucide-react 1.14.0 → 1.16.0, react-router 7.15.0 → 7.15.1, vite 8.0.11 → 8.0.13, @vitejs/plugin-react 6.0.1 → 6.0.2, eslint 10.3.0 → 10.4.0, typescript-eslint 8.59.2 → 8.59.3, @types/node 25.6.2 → 25.8.0. (lucide-react, react-router, and vite ship in the dashboard bundle; the rest are dev/build tooling.)
  • GitHub Actions version bumps. actions/dependency-review-action v4 → v5; actions/setup-node v5 → v6.

Fixed

  • CI Bun version aligned to 1.3.x and the Dependabot lockfile auto-sync repaired. Three workflows (ci.yml, sync-bun-lock.yml, publish-portal.yml) pinned BUN_VERSION: "1.2.x" while the committed bun.lock is in the Bun 1.3 text-lockfile format (configVersion), the Docker dashboard build runs oven/bun:1, and local dev runs 1.3.x. The skew made every grouped Dependabot npm bump fail the Frontend (build + lint) job's tsc --noEmit under 1.2.x's transitive resolution — a duplicate vite Plugin type identity surfacing as TS2321 Excessive stack depth / TS2769 on vite.config.ts. Separately, sync-bun-lock.yml ran bun install --no-save, which never writes bun.lock, so the auto-sync step silently no-op'd and every frontend dependency PR landed with a stale lockfile that broke CI's bun install --frozen-lockfile. All three pins are now "1.3.x" and the sync step drops --no-save so the refreshed lockfile is actually committed back to the PR branch.

[0.2.1] - 2026-05-11

Removed

  • Section-header / banner comment noise (~30 lines). Cleared the empty-divider // ────────────────────────────────────────────────── separators in DashboardMessagesController, DashboardAnalyticsController, DashboardEndpointController (5 banners), and the // ============================================================ banner pairs in WebhookEngine.Sdk/Models.cs (5 banners — Response envelope, Event Types, Endpoints, Messages, Common query parameters). All carried no WHY content; class names and IDE folding already cover navigation. Section-titled banners in test files (// ── Read paths ──, // ── Plumbing ──, etc.) are preserved as deliberate test-grouping markers.

Changed

  • PATCH /api/v1/portal/endpoints/{id} replaces PUT. Portal endpoint update was tagged [HttpPut] but its body semantics were partial-replace (every field optional, only non-null fields applied) — that is the contract for PATCH, not PUT. Switching the method aligns the wire surface with the actual behaviour and avoids confusing REST consumers that expect PUT to be full-replace. The route, request shape, and r

Releases

  • WebhookEngine v0.3.0

    WebhookEngine v0.3.0

    SDK feature expansion, a breaking realignment of three response-model properties, full SDK test coverage (32 new cases), portal client correctness fixes, and a hardened CI/security baseline. Test suite grows from 280 to 312. Breaking change for SDK consumers reading EndpointResponse.CustomHeadersJson, EndpointResponse.MetadataJson, or MessageAttemptResponse.RequestHeadersJson as Dictionary<string,string>? — those properties now return JsonElement / JsonElement? to match the wire format. The v1 route prefix and Standard Webhooks signature surface are preserved.

    Features / Fixes / Changes

    Added

    • EndpointClient.TestAsync in the SDK: EndpointClient.TestAsync(endpointId, request) covers the one live /api/v1/* route that had no SDK binding. Returns the live response and the exact signed request via new models TestEndpointRequest, EndpointTestResult, and EndpointTestRequestPreview.
    • SDK request models now expose all API fields: CreateEventTypeRequest/UpdateEventTypeRequest.IdempotencyWindowMinutes and CreateEndpointRequest/UpdateEndpointRequest.{AllowedIps, TransformExpression, TransformEnabled} — previously these fields were accepted by the API but unreachable from the SDK.
    • First WebhookEngine.Sdk.Tests project (32 cases): covers WebhookVerifier constant-time HMAC across tolerance, secret-encoding (whsec_ vs base64), multi-signature, tamper, and missing-field cases, plus a stub-HttpMessageHandler contract suite that deserializes real API envelopes through the client so response-DTO drift now fails CI.

    Changed

    • SDK response models realigned with the engine DTOs (breaking for three properties): EndpointResponse.CustomHeadersJson / MetadataJson and MessageAttemptResponse.RequestHeadersJson change from Dictionary<string,string>? to JsonElement / JsonElement?. The old dictionary type silently dropped the entire field for any non-string value; the new types match the wire format exactly. EndpointResponse also gains AllowedIps, TransformExpression, TransformEnabled, TransformValidatedAt; EventTypeResponse gains IdempotencyWindowMinutes — all were sent on the wire but silently dropped before this release.
    • Concurrency regression tests on real PostgreSQL: new Testcontainers tests cover the idempotency UNIQUE race (23505 on N concurrent inserts of the same key), the FOR UPDATE SKIP LOCKED dequeue (K workers never double-claim), and the Mark*Async CAS guard. Worker helper methods are now internal and exercised directly so a production regression fails CI rather than silently passing a logic copy.

    Fixed

    • @webhookengine/endpoint-manager portal client realigned: three concrete defects against a real engine — updateEndpoint() sent PUT instead of PATCH (every update failed); the client read a non-existent isActive flag instead of the engine's status string (badge always showed "Disabled"); it read customHeaders instead of customHeaderNames (silently wiped headers on every save). Types now mirror the engine; a contract test prevents this class of drift from shipping again.
    • Dependabot lockfile-sync now re-triggers CI: sync-bun-lock.yml previously pushed with GITHUB_TOKEN, which GitHub's recursion guard blocks from triggering new runs — leaving PRs blocked on required checks until manual close/reopen. The push now uses a short-lived GitHub App installation token so the sync commit re-triggers checks automatically.
    • Dependabot lockfile auto-sync extended to all workspace members: the pull_request_target trigger previously only watched src/dashboard/package.json; bumps in packages/endpoint-manager/package.json silently left a stale bun.lock and broke CI.
    • Documentation accuracy pass: README docker run example corrected (ConnectionStrings__Default, port 5100); docs/PRD.md, docs/ROADMAP.md, docs/ARCHITECTURE.md, and AGENTS.md synced to current state.

    Security

    • Repository security baseline hardened: added SECURITY.md with supported-versions policy and private-vulnerability-reporting flow. Added explicit permissions: contents: read to ci.yml and release.yml. Extended Dependabot to the /packages/endpoint-manager npm workspace. Added timeout-minutes to CI jobs.
    • Secret-scanning false-positive suppression: .github/secret_scanning.yml adds paths-ignore for docs/**, samples/**, tests/**, and **/*.md — WebhookEngine's whsec_ examples collide with the Stripe webhook-secret pattern; source paths remain scanned.

    Quick Start

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

    Dashboard at http://localhost:5100 — login [email protected] / changeme (reset before exposing publicly).

    Links

    Open on GitHub
  • WebhookEngine v0.2.2

    WebhookEngine v0.2.2

    Maintenance patch — a full dependency refresh across NuGet, npm, Docker base images, and GitHub Actions, plus a CI fix that realigns the Bun toolchain (1.2.x → 1.3.x) and repairs the Dependabot lockfile auto-sync. No user-visible behaviour changes and no breaking API changes; the v1 route prefix and Standard Webhooks signature surface are unchanged.

    Features / Fixes / Changes

    Security

    • Docker base image digest bumps. dotnet/aspnet (60eb0311e37a82), dotnet/sdk (8a90a47dc8430e), and oven/bun (4de47535acc90a) refreshed to pick up the latest patched Alpine layers in the published image.

    Changed

    • NuGet runtime dependency refresh. Microsoft.AspNetCore.OpenApi, Microsoft.EntityFrameworkCore (+ .Relational, .Design), Microsoft.Extensions.Http, .Hosting.Abstractions, and .DependencyInjection bumped 10.0.7 → 10.0.8; Scalar.AspNetCore 2.14.11 → 2.14.14. Test-only: FluentAssertions 8.9.0 → 8.10.0, coverlet.collector 10.0.0 → 10.0.1, Microsoft.AspNetCore.Mvc.Testing / EntityFrameworkCore.InMemory 10.0.7 → 10.0.8, Testcontainers.PostgreSql 4.11.0 → 4.12.0.
    • npm / frontend dependency refresh. lucide-react 1.14.0 → 1.16.0, react-router 7.15.0 → 7.15.1, vite 8.0.11 → 8.0.13, @vitejs/plugin-react 6.0.1 → 6.0.2, eslint 10.3.0 → 10.4.0, typescript-eslint 8.59.2 → 8.59.3, @types/node 25.6.2 → 25.8.0. (lucide-react, react-router, and vite ship in the dashboard bundle; the rest are dev/build tooling.)
    • GitHub Actions version bumps. actions/dependency-review-action v4 → v5; actions/setup-node v5 → v6.

    Fixed

    • CI Bun toolchain aligned to 1.3.x and the Dependabot lockfile auto-sync repaired. ci.yml, sync-bun-lock.yml, and publish-portal.yml pinned BUN_VERSION: "1.2.x" while the committed bun.lock is in Bun 1.3 text-lockfile format, the Docker dashboard build runs oven/bun:1, and local dev runs 1.3.x. The skew failed every grouped Dependabot npm bump at tsc --noEmit under 1.2.x's transitive resolution (a duplicate vite Plugin type identity in vite.config.ts). Separately, the lockfile-sync step ran bun install --no-save, which never writes bun.lock, so the auto-sync silently no-op'd and frontend dependency PRs landed with stale lockfiles that broke bun install --frozen-lockfile. All three pins are now 1.3.x and the sync step writes the refreshed lockfile back to the PR branch.

    Quick Start

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

    Dashboard at http://localhost:5100 — login [email protected] / changeme (reset before exposing publicly).

    Links

    Open on GitHub
  • WebhookEngine v0.2.1

    WebhookEngine v0.2.1

    Patch release closing the v0.2.0 portal audit follow-up: three P0 security hardening fixes, 23 new tests filling the portal coverage gaps, four P1 behaviour corrections, two ADRs locking in portal architecture decisions, full API and architecture documentation for the portal stack, and build hygiene (Docker Hub sync hard-failure, comment-noise removal).

    Features / Fixes / Changes

    Security

    • Portal rate-limit enforcement on mutating routes. PortalEndpointsController now carries [EnableRateLimiting("send-by-appid")] at the controller level; a leaked portal token could previously spam /test (real outbound HTTP POST) without sharing the per-tenant rate-limit budget.
    • JWT parser size cap — DoS amplification path closed. PortalTokenAuthMiddleware now rejects Bearer payloads larger than 8 KiB before the JWT parser runs (down from the .NET default ~250 KiB); oversized tokens return 401 immediately.
    • PortalLookupCache atomic CTS swap — race window closed. Set now uses AddOrUpdate to atomically swap and dispose the previous CancellationTokenSource, preventing a racing InvalidateApplication from binding a fresh cache entry to a disposed token.
    • Portal CORS preflight deny-cache. PortalCorsMiddleware now caches both allow and deny outcomes for the signing-key lookup TTL (default 60 s), removing a low-effort DB hammer vector from repeated OPTIONS against disallowed origins.

    Behaviour

    • PATCH /api/v1/portal/endpoints/{id} replaces PUT. The route's partial-replace semantics were always PATCH; the [HttpPut] attribute was a mislabel. The <EndpointManager /> component already issues PATCH.
    • Portal disable preserves AllowedPortalOriginsJson. Disabling an app's portal now only revokes the signing key; the operator-curated CORS allowlist is kept so a re-enable does not require re-entering origins.
    • Validator drift consolidated via EndpointValidationRules. Six shared extension methods replace per-validator duplicates across the four admin and two portal endpoint validators — a single source of truth for rule tightening going forward.
    • npm publish workflow (publish-portal.yml). Fires on portal-v* tags; publishes @webhookengine/endpoint-manager with sigstore provenance and a private:true guard.

    Tests

    • 23 new tests closing v0.2.0 portal coverage gaps. PortalCorsMiddlewareTests (7 facts), PortalLookupCacheTests (5 facts), PortalOriginsAllowlistE2ETests (7 facts, Testcontainers against real PostgreSQL JSONB), plus cross-tenant guard and empty-capabilities defense-in-depth facts in PortalEndpointsControllerTests. Total test count: 279.

    Docs

    • docs/API.md §3.8 — Portal API reference. Covers HS256 JWT contract, capability scopes, per-app CORS, every /api/v1/portal/* route, the portal-specific error code table, and an end-to-end Node.js + cURL probe.
    • docs/ARCHITECTURE.md §4.3 — Portal token authentication. Documents middleware ordering, PortalLookupCache TTL + atomic-CTS-swap behaviour, and JWT validator defense-in-depth choices.
    • ADR-004 — Portal signing key storage. Locks in the plaintext varchar(64) decision, no-grace rotation lifecycle, and one-shot reveal contract.
    • ADR-005 — Portal CORS preflight deny-cache TTL. Locks in the PortalAuth:LookupCacheTtlSeconds-symmetric TTL and documents why no synchronous invalidation hook is needed.
    • docs/RELEASE.md §1 updated. DOCKERHUB_TOKEN now documents all three required scopes to eliminate the "release ran but Docker Hub overview is stale" debugging session.

    Infrastructure / Build

    • release.yml continue-on-error workaround removed. The Sync Docker Hub description step now hard-fails on scope misconfiguration instead of silently succeeding.
    • samples/portal-host/ reference application added; docs/PORTAL.md §5 component usage section completed.
    • Section-header comment noise removed (~30 lines of banner separators with no WHY content cleared from controllers and SDK models).

    Quick Start

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

    Dashboard at http://localhost:5100 — login [email protected] / changeme (reset before exposing publicly).

    Links

    Open on GitHub
  • @webhookengine/endpoint-manager v0.1.0

    @webhookengine/endpoint-manager v0.1.0

    Initial public release of the embeddable customer portal React component for WebhookEngine. Pairs with engine v0.2.0+ (which exposes the /api/v1/portal/* route group). ESM-only, peer deps react ^19 and react-dom ^19, zero runtime dependencies, ~14.2 KB gz JS + ~4.3 KB gz CSS.

    Features / Fixes / Changes

    Added

    • <EndpointManager />: the headline embeddable React component. Wraps a self-contained portal that authenticates against a host SaaS-minted HS256 JWT and serves a customer-facing endpoint management UI. Props: baseUrl, token, appId, capabilities, theme, className, onError, onUnauthorized.
    • <EndpointList />: paginated table of endpoints with status badges, capability-gated [+ New endpoint] plus per-row Edit / Enable / Disable / Delete / Test / Attempts actions.
    • <EndpointEditor />: modal-style overlay for create + edit. URL (HTTPS), description, custom headers (key/value editor), event-type filter, secret override (whsec_ prefix + 32+ char client-side check). Server validation fieldErrors route to per-field inline messages. Field narrowing enforced at the DTO level — transformExpression / transformEnabled / allowedIpsJson are silently dropped on write.
    • <EndpointTester />: modal opened from the Test row action. JSON payload validation on blur, color-coded response panel (status + latency + body, collapsed if >500 chars), collapsible signed-request preview showing the URL + headers (webhook-id / webhook-timestamp / webhook-signature) + body the receiver actually HMAC-verifies.
    • <AttemptList />: modal opened from the Attempts row action. Paginated delivery history with relative + absolute timestamps, success / failure status badges, HTTP code, latency, expandable response excerpts.
    • createPortalClient(): fetch wrapper, zero runtime dependencies. Bearer auth, ApiEnvelope unwrap, 4xx/5xx → PortalError with code + status + fieldErrors, 401 hooks onUnauthorized for token re-mint flows. Methods: listEndpoints, getEndpoint, createEndpoint, updateEndpoint, deleteEndpoint, enableEndpoint, disableEndpoint, testEndpoint, listAttempts, listEventTypes.
    • PortalCapability union and full TypeScript types for PortalAppState, PortalEndpointSummary, PortalEndpointDetail, PortalAttempt, PortalTestResult, PortalListResult<T>, PortalError, PortalClientOptions, EndpointManagerProps.
    • Tailwind 4 internal compile pipeline: dist/style.css ships pre-compiled with the package. The @theme block defines --color-whe-* tokens (background, text, border, accent, success, danger, warning) — consumers override at :root or .whe-portal scope to re-theme without touching component code.
    • 42-test vitest suite covering the client contract, capability gating, field-narrowing, JSON validation, secret-override entropy floor, signed-request preview, status badge color-coding, and pagination boundaries.
    • samples/portal-host/ reference app in the engine repo demonstrating consumer integration: Vite + mocked fetch + browser-side JWT mint (DEMO ONLY — production minting belongs on the host SaaS's own backend).

    Notes

    • Engine compatibility: v0.2.0 or later. Earlier engines lack the /api/v1/portal/* surface.
    • JWT requirements: HS256, signed with the per-app PortalSigningKey (rotated from the engine's operator dashboard). Lifetime cap defaults to 15 min on the engine side. The component never sees the signing key — only the bearer token.
    • Provenance: Published with sigstore attestation via --provenance.

    Links

    Open on GitHub
  • WebhookEngine v0.2.0 — embeddable customer portal (engine half)

    WebhookEngine v0.2.0

    The first minor release. Adds an embeddable customer-facing portal: SaaS operators can now hand customers a self-service <EndpointManager /> React component that runs against a narrowed /api/v1/portal/* API surface, scoped per-application via short-lived HS256 JWTs minted by the host SaaS backend. The engine never mints these tokens — it only verifies them — and the per-app signing key is generated, rotated, and revoked from the operator dashboard. No breaking API changes — the v1 route prefix and Standard Webhooks signature header names are preserved. Test count moved from 215 to 252.

    Features / Fixes / Changes

    Added

    • Embeddable customer portal — engine half (B1 Steps 2-4): new Application.PortalSigningKey (HS256 secret, 64-char varchar) and Application.AllowedPortalOriginsJson (JSONB) columns; new PortalTokenAuthMiddleware validates short-lived HS256 JWTs (algorithm-pinned, 15-minute lifetime cap, capability-scoped via endpoints:read|write|test and attempts:read); new PortalCorsMiddleware does per-application dynamic CORS with RFC 6454-compliant ordinal-case-insensitive origin matching; new /api/v1/portal/* route group exposes a narrowed CRUD-and-test surface that silently strips admin-only fields (transformExpression, allowedIpsJson) on writes and never returns the signing key.
    • Embeddable customer portal — operator dashboard (B1 Step 5): new DashboardPortalController (/api/v1/dashboard/applications/{appId}/portal/...) with 5 cookie-authed actions (read, enable, rotate, disable, update-origins). New <PortalAccessModal /> React component opened from the Applications page row actions: enable / rotate / disable controls with show-once secret reveal, chip-list editor for allowed CORS origins, copy-paste embed snippet for the host SaaS. Audit log records every mutating action with PortalSigningKey redacted to a portalEnabled boolean — the literal secret never enters the snapshot. Cache invalidation via PortalLookupCache.InvalidateApplication(appId) after every mutating write so rotations take effect within milliseconds rather than within the 60-second cache TTL.
    • Application.PortalRotatedAt: new column for surfacing "last rotated at" in the dashboard portal-management UI.
    • MessageRepository.ListAttemptsByEndpointAsync / CountAttemptsByEndpointAsync: drives the portal's per-endpoint attempt history feed; uses the existing idx_attempts_endpoint_status covering index, no new migration.
    • Bun workspaces (B1 Step 1): root package.json declares ["src/dashboard", "packages/*"] so the upcoming @webhookengine/endpoint-manager package can land at packages/endpoint-manager/ without a second migration. Single bun.lock at the workspace root; Dockerfile and CI workflow updated to follow.

    Changed

    • AuditLogsController no longer bypasses the repository pattern: new AuditLogRepository.ListAsync(...) carries the filter chain and pagination; the controller keeps the JSON hydration since that is HTTP response-shaping, not persistence. Behavior unchanged.
    • Dependabot npm PRs auto-sync bun.lock via a new pull_request_target-triggered workflow gated on github.actor == 'dependabot[bot]'. Eliminates the manual bun install + commit + push that every minor / patch frontend bump previously required.
    • Documentation drift sync: CLAUDE.md and README.md stack lines updated to match src/dashboard/package.json (TypeScript 6 / Vite 8 / TanStack Query 5; previous wording said TypeScript 5.9 / Vite 7). ADR-003 (payload transformation) flipped from Proposed to Accepted with an Implementation section recording the three-phase rollout that shipped in v0.1.4.
    • Dependency refresh: tailwindcss and @tailwindcss/vite 4.2.4 → 4.3.0 (with the transitive @tailwindcss/node and @tailwindcss/oxide platform binaries).

    Security

    • HS256-only algorithm allowlist on portal JWTs: ValidAlgorithms = [HmacSha256] is enforced via Microsoft.IdentityModel.Tokens 8.17.0; alg=none and alg=HS384 / HS512 tokens are rejected with PORTAL_AUTH_INVALID_SIGNATURE. The catch-ladder absorbs algorithm-rejection exceptions without echoing the rejected algorithm name in the error response.
    • Per-app dynamic CORS with explicit allowed-origins enumeration (no wildcards); PortalCorsMiddleware echoes the validated request Origin (never *) and is RFC 6454-compliant case-insensitive on host comparisons.
    • App-scope isolation across the portal surface: every portal route reads AppId from the JWT, never from query / body / route. Cross-tenant probes return 404 PORTAL_NOT_FOUND (not 403) so the response shape doesn't leak the existence of cross-tenant resources.
    • SecretOverride entropy floor on portal writes: the portal Create / Update endpoint validators require the whsec_ prefix and a 32-128 char range so a customer cannot silently downgrade their HMAC secret to password123.
    • Audit redaction: DashboardPortalController writes audit-log snapshots with PortalSigningKey reduced to a boolean portalEnabled flag; the literal secret never enters before_json / after_json. Verified by a load-bearing negative test that scans the column for whsec_ after a real enable call.

    Quick Start

    docker pull voyvodka/webhook-engine:0.2.0
    docker compose -f docker/docker-compose.yml up -d
    

    The app starts on http://localhost:5100. Dashboard login: [email protected] / changeme. Portal access for an application is enabled from the dashboard's Applications page → row actions → Portal access.

    Links

    Open on GitHub
  • WebhookEngine v0.1.6

    WebhookEngine v0.1.6

    Feature & polish cut covering eight new capabilities (per-resource overrides, IP allowlist, audit log, endpoint test webhook, SignalR endpoint health, validate-time URL guard), three rounds of dashboard polish (a11y, UX, TanStack Query data layer), three reviewer-finding fixes (transient DNS retry, cascade delete, polling debounce), and a backend correctness pass on the IP allowlist matcher, application rate-limiter sweep, and endpoint health tracker. No breaking API changes — the v1 route prefix and Standard Webhooks signature surface are preserved. Test count moved from 211 to 215.

    Features / Fixes / Changes

    Added

    • Endpoint test webhook (F1): POST /api/v1/dashboard/endpoints/{id}/test fires a customizable, fully-signed webhook to the endpoint URL without enqueueing a real Message, and returns the receiver's response plus the exact request that was sent. Dashboard endpoint editor carries a Send test drawer.
    • Per-application rate-limit override (F6): Application.RateLimitPerSecond 1-second sliding-window override complements the per-endpoint per-minute gate; idle-evicted at 15 minutes.
    • Per-application retention overrides (F3): Application.RetentionDeliveredDays and RetentionDeadLetterDays override WebhookEngine:Retention defaults per tenant; the cleanup worker partitions its sweep accordingly.
    • Per-event-type idempotency window (F4): EventType.IdempotencyWindowMinutes overrides the per-app default for tighter or looser dedupe per event family.
    • Per-endpoint IP allowlist (F8): Endpoint.AllowedIpsJson carries a CIDR positive-list (IPv4 + IPv6); deliveries only proceed when every resolved address sits inside at least one allowed CIDR.
    • Append-only audit log (F9): Admin actions across applications, endpoints, event types, replay, retry, and rotate-key write a forensic row to the new audit_logs table with before / after snapshots and request_id. GET /api/v1/dashboard/audit exposes a paginated, filterable view. The table holds no FKs — rows survive cascades.
    • SignalR endpoint health channel (F7): DeliveryHub broadcasts EndpointHealthChanged(endpointId, status, circuitState, consecutiveFailures, cooldownUntilUtc) whenever EndpointHealthTracker mutates an endpoint's circuit or visible status. Dashboard EndpointsPage consumes the event and invalidates its cache.
    • TanStack Query dashboard data layer (F12): Every dashboard page (Dashboard, Messages, Applications, Endpoints, EventTypes, DeliveryLog) routes server data through useQuery / useMutation. Manual setInterval polling and the smart-debounce shim are gone in favor of cache-aware refetching driven by SignalR invalidation. EndpointsPage initial chunk drops from ~1.5 MB to ~20 kB (CodeMirror lazy-loaded).

    Changed

    • Validator chain rejects private-IP endpoint URLs at create / update (F2). Same SSRF rules already enforced at delivery (ConnectCallback) now run at validate time too — a misconfigured URL is rejected before the row exists.
    • Modal a11y, mobile, dvh (DPR-1 + DPR-3): role="dialog", ARIA wiring, focus trap, max-h-[85dvh], mobile filter md:grid-cols-3, URL field server-side errors as field-scoped messages, payload editor errors split out, SignalR Live / Offline pill in the EndpointsPage header.
    • Dashboard consolidation (DPR-2): Shared StatusBadge, inputClasses, and CodeMirror editorTheme. CodeMirror is React.lazy() — the 1.5 MB chunk loads only when the endpoint editor mounts. parseError returns an ApiError with optional fieldErrors.
    • Backend polish (R2 + R4 + R5): IpAllowlistMatcher.AllAddressesAllowed short-circuits empty allowlists before the empty-resolution deny branch (load-bearing ordering). ApplicationRateLimiter._lastSweepTicks is Volatile.Read + CAS so torn 32-bit reads can't spawn back-to-back sweeps. EndpointHealthTracker.WithEndpointLockAsync no longer double-fetches the endpoint row.
    • Dependency refresh: Backend — Scalar.AspNetCore 2.14.10 → 2.14.11, coverlet.collector 8.0.1 → 10.0.0. Frontend — react / react-dom / react-is 19.2.5 → 19.2.6, react-router 7.14.2 → 7.15.0, @codemirror/view 6.41.1 → 6.42.1, vite 8.0.10 → 8.0.11, plus @tanstack/react-query 5.100.9 (drives F12).

    Fixed

    • Transient DNS failures retry within budget (R1): SocketException / ArgumentException from the IP-allowlist resolution now route through MarkFailedForRetryAsync instead of dead-lettering on first miss; only after the retry budget is exhausted does the message dead-letter.
    • Application / endpoint deletion cascades to bound messages (R3): Message → Application and Message → Endpoint foreign keys carry ON DELETE CASCADE. Migration 20260508081704_CascadeMessageDeleteOnAppAndEndpoint is hand-written SQL because EF doesn't diff OnDelete changes.
    • Modal focus trap, awaited refetches, SignalR cache invalidation on reconnect (DPR-1): Skeleton loaders carry aria-busy="true"; mutating actions await their refetch before closing modals; useDeliveryFeed.onreconnected resets lastHealthChange so a stale event from before the disconnect doesn't double-fire.
    • Smart-debounced dashboard polling (R6) folded into F12: SignalR events now invalidate cache keys; TanStack Query handles the refetch cadence.

    Security

    • Endpoint URL DNS resolution at validator chain (F2): Same private-IP rules already enforced at delivery time now run at create / update — a misconfigured URL is refused before the row exists.
    • Per-endpoint IP allowlist (F8): Opt-in CIDR positive-list at delivery time; transient resolver failures retry within the message's normal budget (R1) so flaky DNS doesn't cascade into dead-letter floods.
    • Append-only audit log (F9): Forensic trail of admin actions; rows survive cascades for post-incident reconstruction.

    Quick Start

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

    Dashboard at http://localhost:5100 — login [email protected] / changeme (reset before exposing publicly).

    Links

    Open on GitHub
  • WebhookEngine v0.1.5

    WebhookEngine v0.1.5

    Post-audit hardening release. A multi-agent deep audit covered security, memory, concurrency, code quality, frontend, operations, timezone correctness, and NuGet SDK compliance; the resulting fixes (F1–F10) plus an idempotency race fix (F7) and an SDK target-framework simplification all land here. No breaking API changes — the v1 route prefix and Standard Webhooks header names are preserved.

    Features / Fixes / Changes

    Added

    • health probes: /health/live (process up) and /health/ready (AppReadinessGate + DbContext.CanConnectAsync) for orchestrators (F5)
    • observability: OpenTelemetry tracing with optional OTLP export via OpenTelemetry:OtlpEndpoint (F5)
    • sdk: WebhookVerifier for Standard Webhooks signature verification — FixedTimeEquals, 5-min default tolerance, whsec_ / base64 secrets, multi-value signatures (F9)

    Changed

    • sdk: target framework simplified from net10.0;net9.0;net8.0 to net10.0 only — pre-v1.0 cleanup, NuGet badge flips to .NET 10.0
    • memory: IMemoryCache size-bounded (SizeLimit = 10_000) with per-app cancellation-token-source invalidation (F4)
    • shutdown: HostOptions.ShutdownTimeout = 45s so in-flight HTTP deliveries can drain on SIGTERM (F10)
    • security: dashboard admin default credentials rejected at startup outside Development (F5)

    Fixed

    • idempotency race: UNIQUE partial index on (app_id, endpoint_id, idempotency_key) + Stripe-style 23505-replay + retention NULL-out for window reuse (F7)
    • duplicate attempts on lock loss: CAS guards on Mark{Delivered,FailedForRetry,DeadLetter}Async returning bool (F2)
    • circuit-breaker race: EndpointHealthTracker mutations serialized via pg_advisory_xact_lock namespace 100_001 (F3)
    • frontend: webhookengine:auth-expired CustomEvent on 401 + RouteErrorBoundary for ChunkLoadError after deploys (F8)
    • memory leaks: HttpResponseMessage disposal + EndpointRateLimiter idle-window eviction (15-min IdleAfter, 5-min sweep) (F9)
    • migration race: startup migration block wrapped in pg_advisory_lock namespace 200_000 (F5)

    Security

    • SSRF + DNS rebinding: PrivateIpDetector blocks RFC1918/loopback/link-local/CGNAT/IPv6 unique-local; SocketsHttpHandler.ConnectCallback pins resolved IP (F1)
    • headers + metrics gate: HSTS / CSP / X-Frame-Options DENY / X-Content-Type-Options / Referrer-Policy / Permissions-Policy; /metrics Bearer-token auth via WebhookEngine:Metrics:ScrapeToken; cookie SecurePolicy = Always outside Dev/Testing (F6)
    • custom headers: CustomHeaderPolicy rejects reserved headers (Authorization, Cookie, Host, Content-*, webhook-*), strips CR/LF, bounds size (F10)

    Migration

    One auto-applying migration ships in this release: 20260505140607_AddIdempotencyUniqueIndex. It NULL-outs any pre-existing duplicate (app_id, endpoint_id, idempotency_key) triples (keeping the most-recent row per group) before creating the unique index, so it cannot fail on legacy data.

    Quick Start

    docker pull voyvodka/webhook-engine:0.1.5
    docker compose up
    

    Links

    Open on GitHub
  • v0.1.4 — Payload Transformation & Multi-arch

    WebhookEngine v0.1.4

    ADR-003 (payload transformation) shipped end-to-end, OpenAPI/Scalar reference surface, NuGet brand icon, Docker Hub overview sync, and the multi-arch image (linux/amd64 + linux/arm64). Alpine base layers refreshed to clear 7 of the 11 Docker Scout CVEs against v0.1.3.

    Added

    • Payload transformation (ADR-003) — full rollout: per-endpoint JMESPath expressions reshape the body before signing and POSTing. Schema (Phase 1), delivery integration with timeout + output-size guards (Phase 2), and a CodeMirror 6-powered dashboard editor with live POST /api/v1/dashboard/transform/validate preview (Phase 3). Fail-open contract — any error falls back to the original payload.
    • OpenAPI document + Scalar interactive reference: /openapi/v1.json + /scalar UI, mapped only in Development and Staging.
    • NuGet brand icon: WebhookEngine.Sdk now ships with the project mark embedded.
    • Docker Hub overview sync: the GitHub README is now synced into the Docker Hub repository overview tab on every release.
    • Security automations: CodeQL (csharp + js-ts), Dependency Review, Dependabot for NuGet/npm/GitHub Actions/Docker base images.

    Fixed

    • Multi-architecture Docker image: linux/amd64 + linux/arm64 manifest list. Previous releases were amd64-only — Apple Silicon Macs and arm64 Linux servers can now docker pull cleanly.
    • Removed phantom unknown / unknown row on Docker Hub: provenance + SBOM attestations explicitly disabled so the tag listing shows only the real platforms.

    Security

    • Alpine base image refresh: openssl/libcrypto3/libssl3 3.5.5-r03.5.6-r0 (1 critical + 5 high CVEs cleared) and musl 1.2.5-r211.2.5-r23 (1 high CVE cleared) via Docker Scout. All Dockerfile FROMs now SHA-pinned for Dependabot tracking.
    • Log-forging hardening: the JMESPath transformer now sanitizes user-supplied expressions before logging (4 cs/log-forging alerts resolved). LogSanitizer moved to WebhookEngine.Core.Utilities so both API and Infrastructure can consume it.

    Changed

    • Frontend toolchain: dashboard package manager migrated from Yarn to Bun 1.2+.

    Removed

    • WebhookEngine.Application project: empty since the CQRS scaffold removal in v0.1.0; cleaned up across the solution and ADR-002.

    Quick Start

    docker pull voyvodka/webhook-engine:0.1.4   # amd64 + arm64
    # or
    docker pull voyvodka/webhook-engine:latest
    
    cd docker && docker compose up -d
    

    Links

    Open on GitHub
  • v0.1.3 — Landing Page & SDK Alignment

    WebhookEngine v0.1.3

    Documentation and packaging update accompanying the new project landing page.

    Changes

    • landing page: new project site at webhook.sametozkan.com.tr — features, quick start, and links to all resources
    • readme: website, Docker Hub, and NuGet links added to the header
    • sdk: version aligned with main project (0.1.3); package project URL updated to the landing page
    • docs: removed stale internal planning files (backlog-v0.1.1, triage-flow, typescript-sdk-demand-criteria)

    Links

    Open on GitHub
  • v0.1.2 — Stabilization Patch

    WebhookEngine v0.1.2

    Bug fixes, dependency updates, and dashboard reliability improvements.

    Fixes

    • circuit-breaker limbo: Messages for circuit-open endpoints no longer get stuck in Pending forever — they now increment attemptCount and eventually dead-letter as expected
    • FluentValidation migration: Replaced deprecated FluentValidation.AspNetCore with custom action filter using FluentValidation.DependencyInjectionExtensions 12.1.1
    • dashboard analytics: Fixed SQL alias casing in timeline query for EF Core mapping
    • SignalR reconnection: Client now retries indefinitely with exponential backoff (capped at 30s) instead of giving up after 4 attempts
    • delivery-log route: Removed broken parameterless /delivery-log nav item that errored without a messageId

    Chores

    • NuGet: EF Core 10.0.5, removed unused Serilog/Http packages from Infrastructure and Worker
    • Frontend: Vite 8, TypeScript 6, lucide-react 1.7, and other dependency upgrades
    • Login page: Removed debug credentials, improved UX with monospace font and error animation

    Links

    Open on GitHub
  • v0.1.1 — Stabilization Patch

    WebhookEngine v0.1.1

    Post-stabilization patch with documentation sync and a SQL mapping fix.

    Fixes

    • fix: PascalCase SQL aliases in DashboardStatsRepository for correct EF Core SqlQueryRaw property mapping

    Documentation

    • Updated AGENTS.md and ARCHITECTURE.md to reflect Phase 3 DashboardController split (4 controllers) and CQRS scaffold removal
    • Fixed dev port references in CONTRIBUTING.md (5100 → 5128 for dotnet run)

    Stabilization Summary (v0.1.0 → v0.1.1)

    This release concludes the v0.1.0 stabilization milestone:

    • 5 phases, 13 plans, 18/18 requirements satisfied
    • Circuit breaker race condition fixed, message status state machine added
    • Per-AppId rate limiting, configurable idempotency window
    • DashboardController split into 4 single-responsibility controllers
    • Dashboard overview query consolidated (9 queries → 1)
    • 3 ADRs written (replay scope, CQRS removal, payload transformation)
    • All 5 GitHub issues closed

    Full Changelog: https://github.com/voyvodka/webhook-engine/compare/v0.1.0...v0.1.1

    Open on GitHub
  • v0.1.0 — Initial Release

    WebhookEngine v0.1.0

    First public release of WebhookEngine — a self-hosted webhook delivery platform.

    Core Features

    • Queue-based delivery — PostgreSQL SKIP LOCKED queue, no Redis/RabbitMQ needed
    • Retry with backoff — 7 attempts with exponential backoff (5s → 24h)
    • Circuit breaker — per-endpoint health tracking with automatic open/close via advisory locks
    • HMAC signing — Svix-compatible Standard Webhooks signatures
    • Rate limiting — per-application token bucket on public endpoints
    • Configurable idempotency — per-app deduplication window (default 24h)
    • Real-time dashboard — React 19 + SignalR live delivery monitoring
    • Prometheus metrics — OpenTelemetry instrumentation built-in
    • Single container — API + workers + dashboard in one Docker image

    Quick Start

    docker pull voyvodka/webhook-engine:0.1.0
    docker compose -f docker/docker-compose.yml up
    

    Dashboard: http://localhost:5100 — Login: [email protected] / changeme

    Links

    Open on GitHub