A minimal webhook delivery platform built with Go and PostgreSQL.
It accepts events, finds subscribed webhooks, queues deliveries, and delivers them asynchronously with retries, HMAC signatures, SSRF protection, and basic observability.
- Webhook registration
- Event subscriptions
- Asynchronous delivery using Go workers
- In-memory job queue
- Retry with exponential backoff and jitter
- Durable delivery state in PostgreSQL
- Delivery lease and stuck-delivery recovery
- HMAC-SHA256 webhook signatures
- SSRF protection for webhook URLs
- API key authentication
- Structured delivery logs
- Prometheus metrics
- Minimal web UI
- Demo subscriber for testing failures and retries
┌─────────────────┐
│ Web UI │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Go API │
└────────┬────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ PostgreSQL │ │ In-Memory │
│ │ │ Queue │
└─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ Workers │
└──────┬──────┘
│
│ HTTP POST
▼
┌─────────────┐
│ Subscriber │
└─────────────┘
Create Webhook
↓
Create Event
↓
Find Subscribers
↓
Create Deliveries in PostgreSQL
↓
Queue
↓
Worker
↓
HTTP POST + HMAC Signature
↓
Subscriber
↓
Success / Retry / Failure
- Go 1.22+
- PostgreSQL
Clone the repository:
git clone <repository-url>
cd webhook-platformCreate a PostgreSQL database:
CREATE DATABASE webhooks;Create .env:
WEBHOOK_API_KEY=my-demo-keyUpdate the database connection if required.
Run the migrations:
psql postgres://postgres:postgres@localhost:5432/webhooks \
-f migrations/001_init.sqlStart the webhook platform:
go run .The platform runs on:
http://localhost:8080
Open the UI:
http://localhost:8080
Start the demo subscriber in another terminal:
go run ./subscriberThe subscriber runs on:
http://localhost:9000
The subscriber verifies the X-Webhook-Signature header and randomly returns:
200 → successful delivery
500 → retry
400 → permanent failure
timeout → retry
This makes it easy to demonstrate the delivery and retry behavior.
POST /webhooks
X-API-Key: my-demo-key
Content-Type: application/jsonExample:
{
"url": "http://localhost:9000/webhook",
"events": ["order.created", "order.updated"]
}The response contains the generated webhook secret.
POST /events
X-API-Key: my-demo-key
Content-Type: application/jsonExample:
{
"event_type": "order.created",
"payload": {
"order_id": "123"
}
}GET /webhooks
X-API-Key: my-demo-keyGET /webhooks/{id}
X-API-Key: my-demo-keyGET /deliveries
X-API-Key: my-demo-keyGET /deliveries/{id}
X-API-Key: my-demo-keyGET /healthGET /metricsEvery delivery contains:
X-Webhook-Signature: sha256=<signature>
The signature is generated using HMAC-SHA256:
HMAC-SHA256(webhook_secret, request_body)
The subscriber verifies the signature before processing the event.
Temporary failures such as:
- HTTP 5xx
- network errors
- timeouts
are retried with exponential backoff and jitter.
Permanent failures such as HTTP 4xx are not retried.
A delivery can make up to 4 attempts.
Run all tests:
go test ./...Run with the race detector:
go test -race ./...The demo subscriber runs on localhost:9000.
For local development, enable:
ALLOW_LOCAL_WEBHOOKS=true