You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WebhookEngine is a self-hosted webhook delivery platform. Single ASP.NET Core host serves the REST API, background delivery workers, and a React dashboard (as static files from wwwroot). PostgreSQL is the only external dependency — used for data storage AND as a job queue via SKIP LOCKED.
# Restore & build entire solution
dotnet build WebhookEngine.sln
# Run the API host (includes worker + serves dashboard)
dotnet run --project src/WebhookEngine.API
# Run all tests
dotnet test WebhookEngine.sln
# Run a single test project
dotnet test tests/WebhookEngine.Core.Tests
# Run a single test by fully qualified name
dotnet test --filter "FullyQualifiedName~ClassName.MethodName"# Run tests matching a pattern
dotnet test --filter "DisplayName~circuit_breaker"# Dashboard (React SPA) — uses Yarn, NOT npmcd src/dashboard && yarn install
cd src/dashboard && yarn dev # dev servercd src/dashboard && yarn build # production build → copies to API/wwwroot# Docker
docker compose -f docker/docker-compose.yml up # production (app + postgres)
docker compose -f docker/docker-compose.dev.yml up # dev (PostgreSQL only)
Code Style — C# Backend
Naming Conventions
Element
Convention
Example
Classes
PascalCase
HttpDeliveryService
Interfaces
I + PascalCase
IMessageQueue, IDeliveryService
Methods
PascalCase + Async suffix
DeliverAsync, SignAsync
Parameters
camelCase
messageId, endpointUrl
Private fields
_ + camelCase
_httpClientFactory
Constants
PascalCase
MaxRetries
Enums
PascalCase (type + values)
MessageStatus.Delivered
Files
Match class name
HmacSigningService.cs
Architecture Patterns
Controller-based: Business logic lives in controllers (Application layer CQRS scaffold removed — see ADR-002)
Repository pattern: One repository per aggregate root in Infrastructure/Repositories/
ASP.NET Core request metrics and .NET runtime metrics included automatically
Structured Logging
Serilog with JSON formatter
Background workers log with correlation context: MessageId, EndpointId, AttemptNumber
Backend Performance & Optimization Notes
PostgreSQL Queue Tuning
Queue polling uses a partial index (idx_messages_queue WHERE status = 'pending') — never remove or alter this index, it is critical for delivery throughput
Delivery Worker dequeues in batches of 10 (LIMIT 10 FOR UPDATE SKIP LOCKED) — this reduces round trips and lock contention
Stale locks (worker crash) are recovered after 5 minutes — messages stuck in sending with locked_at older than 5 min get reset to pending
Single-instance throughput target: 100-500 deliveries/sec; sustained >1000/sec requires migrating to Redis/RabbitMQ via IMessageQueue interface
HTTP Client Rules
Always use IHttpClientFactory — never instantiate new HttpClient() directly; this causes socket exhaustion and DNS caching issues
Delivery timeout is configured via DeliveryOptions (default 30 seconds) — set at the named client level ("webhook-delivery")
Catch TaskCanceledException for timeouts, HttpRequestException for connection failures — never let these propagate uncaught
EF Core & Database Access
Use AsNoTracking() on all read-only queries (list endpoints, message logs, dashboard stats) — avoids unnecessary change tracker overhead
Guard against N+1 queries — use .Include() for related entities or project with .Select() to DTOs
Raw SQL is acceptable for performance-critical paths (queue polling with SKIP LOCKED, dashboard aggregation queries)
Never call SaveChanges inside a loop — batch operations into a single unit of work
Memory & Resource Management
Idle memory target: < 256MB for the entire host process
Truncate response_body in message_attempts to 10KB max — prevents storage explosion from large error pages
Stream large payloads instead of buffering entirely in memory
All background workers must respect CancellationToken — propagate it through every async call for graceful shutdown support
Background Worker Rules
Never throw exceptions from IHostedService.ExecuteAsync — catch, log (Serilog structured), and continue the loop
Always check circuit breaker state before attempting delivery — skip endpoints with open circuits
Workers should log with correlation context: MessageId, EndpointId, AttemptNumber for traceability
On graceful shutdown (CancellationToken triggered), finish in-flight deliveries but stop dequeuing new messages
Data Retention & Cleanup
Delivered messages are purged after 30 days, dead-letter after 90 days (configurable)
A daily cleanup background job (RetentionCleanupWorker) runs at 03:00 UTC — deletes expired records in batches to avoid long-running transactions and table locks
Without retention cleanup, the messages and message_attempts tables will grow unbounded and degrade query performance
Important Architectural Decisions
Single process: API + Workers + Dashboard all in one ASP.NET Core host
PostgreSQL as queue:SELECT ... FOR UPDATE SKIP LOCKED — no Redis/RabbitMQ needed for MVP