webhooksmith
Webhook delivery engine for Rust applications backed by Postgres.
webhooksmith stores outgoing webhooks in your existing Postgres database and delivers them via a background worker. No additional infrastructure is required beyond Postgres.
When to use this
- You need to send webhooks to external endpoints from your application
- You want delivery guarantees — events are persisted before delivery is attempted
- You already run Postgres and do not want to add another service
- You need the transactional outbox pattern: your business data and the webhook event written in the same database transaction, so neither exists without the other
Quick start
[]
= "0.1"
= { = "1", = ["full"] }
= "1"
use WebhookEngine;
use json;
async
Transactional outbox
The outbox pattern ensures your business data and the webhook event are written atomically. If the process crashes after the database write but before delivery, the event is not lost — the worker picks it up on the next restart.
let mut tx = engine.pool.begin.await?;
// Your business logic and the webhook in the same transaction
query!
.execute
.await?;
engine
.send_in_tx
.await?;
tx.commit.await?;
// Webhook is queued only if this commit succeeds
broadcast_in_tx works the same way for fan-out to all endpoints.
Graceful shutdown
The in-flight delivery batch completes before the worker exits. No new batch is claimed after the shutdown signal fires. Events not yet claimed stay in the database and are picked up on next startup.
// Shut down on Ctrl-C or SIGTERM:
engine.run_graceful.await;
Idempotency keys
If your code retries on network errors, the same event can be enqueued twice. Idempotency keys prevent this.
// Safe to call multiple times — only one event is created
engine
.send_idempotent
.await?;
The key is scoped per endpoint, so the same key can be used independently across endpoints (useful with broadcast_idempotent).
Receiving webhooks
webhooksmith-axum provides a tower middleware and axum extractors for verifying incoming webhook signatures. See webhooksmith-axum.
Running the demo
The demo starts a real axum receiver, sends three events including one via the transactional outbox, simulates a failure and retry, and prints the full delivery log.
Features
- Postgres-backed persistence — events survive process restarts
- Transactional outbox —
send_in_tx/broadcast_in_txwrite atomically with your data - Fan-out —
broadcastdelivers one event per enabled endpoint in a single SQL statement - Idempotency keys —
send_idempotent/broadcast_idempotentdeduplicate per (endpoint, key) - Exponential backoff with full jitter — automatic retry on failure
- Dead-letter queue — events that exhaust retries move to DLQ;
retry_deadrequeues them - Graceful shutdown —
run_graceful(signal)drains the current batch before stopping - Worker crash recovery —
delivering_sincetimestamp lets the reaper reset stuck events - HMAC-SHA256 signing — Svix-compatible
v1,<hex>signature format - SSRF protection — private IPs, loopback, and link-local addresses blocked at registration
- Redirect protection — HTTP redirects are not followed during delivery
- Response body limit — streamed at most 4 KB regardless of response size
- Multi-worker safe —
SELECT FOR UPDATE SKIP LOCKEDprevents double-processing
Configuration
let engine = builder
.database_url // or .pool(existing_pool)
.batch_size // events per worker cycle (default: 50)
.poll_interval // idle sleep between cycles (default: 500ms)
.build
.await?;
Endpoint options:
engine.register_with.await?;
Requirements
- Rust 1.75+
- Postgres 14+ (uses
gen_random_uuid(),FOR UPDATE SKIP LOCKED, partial unique indexes)
License
MIT OR Apache-2.0