Deploy and configure a webhook broker on Fly.io — manage webhooks, event routing, retry logic, and observability for serverless event distribution.
Install
npx skillscat add lightheart-ventures/aish/webhook-broker-flyio Install via the SkillsCat registry.
Webhook Broker on Fly.io
Deploy and operate a webhook broker — a serverless event distributor that accepts incoming webhooks, queues them, and forwards to multiple subscribers with automatic retries, exponential backoff, and full observability.
Use this skill when:
- Setting up a new webhook broker on Fly.io from scratch
- Configuring webhook routing and subscriber management
- Implementing retry logic and dead-letter queues
- Monitoring broker health and event throughput
- Troubleshooting failed deliveries or stuck queues
- Scaling the broker across regions or deployment tiers
§ 0 — Quick Start: Deploy in 5 Minutes
Prerequisites
- Fly.io account with flyctl installed: https://fly.io
- GitHub repo with code (can clone the webhook-broker template)
- Node.js 18+ or Rust 1.75+ (pick your runtime)
1. Clone or create the broker scaffold
Option A: Use an existing template (fastest)
# If a template exists in the aish org, clone it:
git clone https://github.com/LightHeart-Ventures/webhook-broker-flyio.git
cd webhook-broker-flyioOption B: Scaffold from scratch
Create a new directory and initialize:
mkdir webhook-broker-flyio
cd webhook-broker-flyio
git initThen create Dockerfile, fly.toml, and app code (see § 1 — Architecture).
2. Create a Fly.io app
flyctl launch
# Follow prompts:
# - App name: "webhook-broker" (or your-org-webhook-broker)
# - Region: select primary region (iad for US-East recommended)
# - Keep defaults for PostgreSQL? (yes if you want persistent storage)This creates fly.toml and sets up secrets.
3. Set environment variables
flyctl secrets set \
LOG_LEVEL=info \
WEBHOOK_SIGNING_KEY="$(openssl rand -hex 32)"4. Deploy
flyctl deployCheck status:
flyctl status
flyctl logsYour broker is live at https://<app-name>.fly.dev.
§ 1 — Architecture Overview
Components
┌─────────────────────────────────────────────────────────────────┐
│ Webhook Broker (Fly.io App) │
├──────────────┬──────────────────┬──────────────────────────────┤
│ │ │ │
│ HTTP API │ Event Queue │ Subscriber Manager │
│ (Accept) │ (In-Memory or │ (Route events to targets) │
│ │ PostgreSQL) │ │
│ │ │ │
└──────────────┴──────────────────┴──────────────────────────────┘
▲ │ │
│ │ ▼
│ [Process Queue] ┌────────────────┐
│ - Retry logic │ Target │
│ - Backoff │ Webhooks │
│ - DLQ │ (POST /notify) │
│ └────────────────┘
│
┌──────────────┐
│ Event Source │
│ (GitHub API, │
│ SaaS, etc) │
└──────────────┘
Flow
- Event Ingestion: POST
/webhooks→ JSON payload + signature - Validation: Check HMAC signature, schema
- Queueing: Store event in queue (in-memory for dev, PostgreSQL for prod)
- Routing: Look up subscribers for this event type
- Delivery: POST to each subscriber with retries
- Observability: Log all events, deliveries, failures to stdout/observability platform
§ 2 — Configuration & Setup
2.1 Environment Variables
| Variable | Purpose | Default | Example |
|---|---|---|---|
PORT |
HTTP server port | 3000 |
3000 |
LOG_LEVEL |
Logging verbosity | info |
debug, info, warn, error |
WEBHOOK_SIGNING_KEY |
HMAC key for signature verification | (required) | hex-encoded-32-bytes |
DATABASE_URL |
PostgreSQL connection (optional) | unset | postgres://user:pass@host/db |
QUEUE_TYPE |
In-memory or PostgreSQL | memory |
memory, postgres |
RETRY_MAX_ATTEMPTS |
Max retries per delivery | 5 |
3–10 |
RETRY_BACKOFF_MS |
Initial backoff (exponential) | 1000 |
500–5000 |
DLQ_ENABLED |
Dead-letter queue for failed events | true |
true, false |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP/HTTP collector endpoint; enables metrics export (see crates/aish-webhook-broker/deploy/signoz/README.md) |
unset | http://localhost:4318 |
2.2 Setting Variables in Fly.io
One-time setup (creates/updates secrets):
flyctl secrets set \
LOG_LEVEL=info \
WEBHOOK_SIGNING_KEY="$(openssl rand -hex 32)" \
RETRY_MAX_ATTEMPTS=5 \
RETRY_BACKOFF_MS=1000 \
DLQ_ENABLED=trueVerify secrets are set:
flyctl secrets listUpdate a single secret:
flyctl secrets set LOG_LEVEL=debugDeploy after config change:
flyctl deploy§ 3 — API Reference
3.1 Register a Webhook
POST /webhooks/register
Register your app to receive events of a specific type.
Request:
{
"webhook_url": "https://your-app.example.com/notify",
"event_type": "order.created",
"retry_policy": {
"max_attempts": 5,
"backoff_ms": 1000
},
"active": true
}Response:
{
"id": "sub_abc123",
"webhook_url": "https://your-app.example.com/notify",
"event_type": "order.created",
"created_at": "2024-01-15T10:30:00Z",
"status": "active"
}3.2 Send a Webhook
POST /webhooks/send
Trigger a webhook delivery to all subscribers of an event type.
Request:
{
"event_type": "order.created",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"order_id": "12345",
"customer": "Alice",
"total": 99.99
}
}Response:
{
"event_id": "evt_abc123",
"queued_at": "2024-01-15T10:30:00Z",
"subscribers_count": 2,
"status": "queued"
}3.3 List Subscribers
GET /webhooks/subscribers?event_type=order.created
Retrieve all subscribers for a given event type.
Response:
{
"subscribers": [
{
"id": "sub_abc123",
"webhook_url": "https://app1.example.com/notify",
"event_type": "order.created",
"active": true,
"created_at": "2024-01-15T10:00:00Z"
},
{
"id": "sub_def456",
"webhook_url": "https://app2.example.com/notify",
"event_type": "order.created",
"active": true,
"created_at": "2024-01-15T10:15:00Z"
}
]
}3.4 Get Delivery Status
GET /webhooks/deliveries/:event_id
Check delivery status for a specific event.
Response:
{
"event_id": "evt_abc123",
"event_type": "order.created",
"sent_at": "2024-01-15T10:30:00Z",
"deliveries": [
{
"subscriber_id": "sub_abc123",
"target_url": "https://app1.example.com/notify",
"status": "success",
"attempts": 1,
"last_attempt": "2024-01-15T10:30:05Z",
"response_code": 200
},
{
"subscriber_id": "sub_def456",
"target_url": "https://app2.example.com/notify",
"status": "retrying",
"attempts": 2,
"last_attempt": "2024-01-15T10:30:30Z",
"next_retry": "2024-01-15T10:31:30Z",
"response_code": 500
}
]
}§ 4 — Deployment & Operations
4.1 Initial Deployment
# 1. Deploy the app
flyctl deploy
# 2. Check app is running
flyctl status
# 3. Check logs
flyctl logs
# 4. Test the app
curl https://<app-name>.fly.dev/health
# Expected: { "status": "ok" }4.2 Scaling
Scale your broker to multiple instances for high throughput:
# Scale to 3 instances (auto-fallback if one fails)
flyctl scale count 3
# Check running instances
flyctl status
# View instance details
flyctl scale show4.3 Database Setup (Persistent Queue)
If using PostgreSQL for the queue:
# Attach a PostgreSQL database to your Fly app
flyctl postgres attach webhook-broker-db
# Set DATABASE_URL secret (should be done automatically)
# Verify it:
flyctl secrets list | grep DATABASE
# Run migrations (if using a migration tool like Flyway or sqlc)
flyctl ssh console
# Inside console:
psql $DATABASE_URL -f migrations/001-init.sql
exit4.4 Monitoring & Logs
Stream live logs:
flyctl logs --followSearch logs for errors:
flyctl logs | grep ERRORExport metrics to observability platform:
If using SigNoz, Datadog, or New Relic, configure OpenTelemetry:
flyctl secrets set \
OTEL_EXPORTER_OTLP_ENDPOINT="https://your-collector.example.com"§ 5 — Troubleshooting
5.1 Events Not Being Delivered
Symptoms: Events are queued but not reaching subscribers.
Checklist:
Check if subscriber is registered:
curl https://<app-name>.fly.dev/webhooks/subscribers?event_type=order.createdExpect a non-empty list.
Check if subscriber URL is reachable:
curl -X POST https://your-subscriber.example.com/notify \ -H "Content-Type: application/json" \ -d '{"test": "payload"}'Check logs for delivery errors:
flyctl logs | grep "delivery failed"Verify HMAC signature on subscriber side — broker signs with
WEBHOOK_SIGNING_KEY:import hmac import hashlib signature = request.headers.get('X-Webhook-Signature') body_bytes = request.data expected = 'sha256=' + hmac.new( WEBHOOK_SIGNING_KEY.encode(), body_bytes, hashlib.sha256 ).hexdigest() assert signature == expected, "Invalid signature"
5.2 High Latency or Timeouts
Symptoms: Deliveries are slow or timing out.
Solutions:
Scale up to more instances:
flyctl scale count 5Increase timeout for slow subscribers:
flyctl secrets set DELIVERY_TIMEOUT_MS=10000 flyctl deployCheck database performance (if using PostgreSQL):
flyctl ssh console psql $DATABASE_URL EXPLAIN ANALYZE SELECT * FROM events WHERE status = 'pending' LIMIT 1000; \q exit
5.3 Memory or Disk Pressure
Symptoms: App crashes, "out of memory" or "disk full" errors.
Solutions:
Check resource usage:
flyctl metricsIf using in-memory queue, switch to PostgreSQL:
flyctl secrets set QUEUE_TYPE=postgres flyctl deployIncrease instance RAM:
flyctl scale memory <amount-in-MB> # e.g., flyctl scale memory 512
§ 6 — Security Best Practices
6.1 HMAC Signing
Always verify webhook signatures on the subscriber side:
Generate a strong key (32 random hex bytes):
openssl rand -hex 32Set in Fly.io:
flyctl secrets set WEBHOOK_SIGNING_KEY="<your-key>"Share with subscribers (via secure channel, never in URL):
# Subscriber stores this securely (env var, secrets manager) export WEBHOOK_SIGNING_KEY="<same-key>"Verify on receipt:
import hmac import hashlib def verify_signature(request): sig = request.headers.get('X-Webhook-Signature', '') body = request.data expected = 'sha256=' + hmac.new( WEBHOOK_SIGNING_KEY.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(sig, expected)
6.2 Rate Limiting
Protect your subscribers from being overwhelmed:
flyctl secrets set \
RATE_LIMIT_PER_SECOND=100 \
RATE_LIMIT_WINDOW_SECONDS=606.3 Secrets Management
Never commit secrets to git:
# .gitignore
.env
.env.local
fly.toml # (if it contains secrets; use fly.toml template instead)Use flyctl secrets for all credentials:
flyctl secrets set VAR=value # Creates on Fly.io
flyctl secrets unset VAR # Removes from Fly.io
flyctl secrets list # Lists all (values hidden)§ 7 — Advanced Topics
7.1 Multi-Region Deployment
Deploy the broker to multiple regions for high availability:
# Add a region
flyctl regions add iad sjc
# Check regions
flyctl regions list
# Scale across regions
flyctl scale count 2 --max-per-region 17.2 Custom Event Types
Define and manage event types:
# Register a new event type (app-specific)
POST /webhooks/event-types
{
"name": "order.created",
"schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"customer": { "type": "string" },
"total": { "type": "number" }
},
"required": ["order_id"]
}
}7.3 Dead-Letter Queue (DLQ)
Events that fail all retries go to the DLQ:
# Enable DLQ
flyctl secrets set DLQ_ENABLED=true
# Query DLQ
GET /webhooks/dlq?limit=100
# Response includes failed events with error details for debugging7.4 Webhook Replay
Replay failed events to subscribers:
POST /webhooks/replay
{
"event_id": "evt_abc123",
"subscriber_id": "sub_def456"
}§ 8 — Integration Examples
8.1 GitHub Webhooks → Your App
Receive GitHub events (push, PR, etc.) and forward to your app:
# 1. Register GitHub as subscriber
curl -X POST https://<broker>.fly.dev/webhooks/register \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://your-app.example.com/github-events",
"event_type": "github.push",
"active": true
}'
# 2. On GitHub repo settings, add webhook:
# Payload URL: https://<broker>.fly.dev/webhooks/send
# Event type: Push events
# Secret: <use WEBHOOK_SIGNING_KEY>
# 3. Broker receives from GitHub, forwards to your-app.example.com8.2 SaaS Webhook Distribution
Use broker to replay webhooks to multiple internal services:
# Register multiple subscribers
for service in order-processor billing-service notification-service; do
curl -X POST https://<broker>.fly.dev/webhooks/register \
-d "{
\"webhook_url\": \"https://$service.internal/webhooks\",
\"event_type\": \"order.created\",
\"active\": true
}"
done
# Send event once
curl -X POST https://<broker>.fly.dev/webhooks/send \
-d '{
"event_type": "order.created",
"event_id": "evt_1",
"data": {"order_id": "123"}
}'
# Broker delivers to all three services with retries§ 9 — Checklist: Running Production
- Secrets set —
WEBHOOK_SIGNING_KEY,LOG_LEVEL,DATABASE_URL(if using DB) - Scaled to ≥2 instances —
flyctl scale count 2 - Database attached (if persistent queue) —
flyctl postgres attach - Monitoring enabled — Logs streaming, metrics exported
- HMAC verification implemented on subscribers
- Rate limiting configured (optional but recommended)
- DLQ enabled for observability of failed events
- Health endpoint accessible —
curl https://<app>.fly.dev/health - Tested with real events — Run end-to-end test before declaring "live"
- Alerts set up — Notify on delivery failures, high error rate, queue backlog
§ 10 — Quick Reference: Common Commands
| Task | Command |
|---|---|
| Deploy | flyctl deploy |
| Check status | flyctl status |
| Stream logs | flyctl logs --follow |
| Set secret | flyctl secrets set KEY=value |
| List secrets | flyctl secrets list |
| Scale instances | flyctl scale count 3 |
| SSH into app | flyctl ssh console |
| Restart app | flyctl restart |
| View metrics | flyctl metrics |
| Attach database | flyctl postgres attach db-name |
| Redeploy after config | flyctl deploy |
Summary
A webhook broker on Fly.io provides:
✅ Reliability — Automatic retries, exponential backoff, DLQ
✅ Scalability — Multi-instance, multi-region deployment
✅ Security — HMAC signing, secrets management, rate limiting
✅ Observability — Logs, metrics, delivery tracking
✅ Simplicity — One-line registration, event-driven architecture
Start with § 0 for quick deployment. Refer to § 2–9 for configuration, troubleshooting, and advanced patterns.
Read the official Fly.io docs for deeper details on deployments, scaling, and PostgreSQL:
https://fly.io/docs
Version: 1.0.0
Last Updated: 2024-01-15
Maintainer: LightHeart Ventures Webhook Team