use std::{
sync::{Arc, Mutex},
time::Duration,
};
use axum::{extract::State, http::StatusCode, routing::post, Router};
use webhooksmith::WebhookEngine;
use webhooksmith_axum::{VerifiedWebhook, WebhookSecretLayer};
use serde_json::json;
const SECRET: &str = "demo-signing-secret-32chars-long";
const DATABASE_URL: &str = "postgres://hooksmith:hooksmith@localhost:5432/hooksmith";
#[derive(Clone, Default)]
struct ReceivedLog(Arc<Mutex<Vec<String>>>);
async fn receive_webhook(
State(log): State<ReceivedLog>,
VerifiedWebhook(payload): VerifiedWebhook,
) -> StatusCode {
let entry = format!(
" [receiver] ✓ event_type={} payload={}",
payload.event_type,
serde_json::to_string(&payload.body).unwrap_or_default(),
);
println!("{entry}");
log.0.lock().unwrap().push(entry);
StatusCode::OK
}
async fn start_axum_receiver(secret: &str) -> (String, ReceivedLog) {
let log = ReceivedLog::default();
let log_clone = log.clone();
let app = Router::new()
.route("/webhooks", post(receive_webhook))
.layer(WebhookSecretLayer::new(secret))
.with_state(log_clone);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let url = format!("http://127.0.0.1:{port}/webhooks");
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
(url, log)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter("webhooksmith=info,demo=info")
.without_time()
.init();
println!("\n╔══════════════════════════════════════╗");
println!("║ webhooksmith demo ║");
println!("╚══════════════════════════════════════╝\n");
println!("[1] Starting axum webhook receiver (webhooksmith-axum)...");
let (receiver_url, log) = start_axum_receiver(SECRET).await;
println!(" Listening at {receiver_url}\n");
println!("[2] Connecting WebhookEngine to Postgres...");
let engine = WebhookEngine::builder()
.database_url(DATABASE_URL)
.allow_insecure_urls() .build()
.await?;
engine.migrate().await?;
println!(" Connected and migrations applied.\n");
println!("[3] Registering endpoint...");
let endpoint = engine.register(&receiver_url, SECRET).await?;
println!(" Endpoint ID: {}\n", endpoint.id);
println!("[4] Dispatching events...");
let e1 = engine
.send("order.created", json!({"order_id": 1001, "total": 49.99}), endpoint.id)
.await?;
println!(" Queued {} (order.created)", e1.id);
let e2 = engine
.send("payment.succeeded", json!({"order_id": 1001, "amount": 49.99}), endpoint.id)
.await?;
println!(" Queued {} (payment.succeeded)", e2.id);
println!("\n[5] Transactional outbox demo...");
let mut tx = engine.pool().begin().await?;
let e3 = engine
.send_in_tx(
"order.shipped",
json!({"order_id": 1001, "tracking": "1Z999AA10123456784"}),
endpoint.id,
&mut tx,
)
.await?;
println!(" Event {} written in transaction (not yet visible)", e3.id);
tx.commit().await?;
println!(" Transaction committed — event now queued.\n");
println!("[6] Retry demo: inserting event to a dead endpoint...");
let dead_endpoint = engine
.register("http://127.0.0.1:19999/dead", SECRET)
.await?;
let e_retry = engine
.send("retry.demo", json!({"attempt": 1}), dead_endpoint.id)
.await?;
engine.run_once().await?;
tokio::time::sleep(Duration::from_millis(100)).await;
let status_after_fail = engine.event(e_retry.id).await?.unwrap();
println!(" After 1st attempt: status={:?}", status_after_fail.status);
sqlx::query!(
"UPDATE webhook_endpoints SET url = $1 WHERE id = $2",
receiver_url,
dead_endpoint.id,
)
.execute(engine.pool())
.await?;
sqlx::query!(
"UPDATE webhook_events SET scheduled_at = NOW() WHERE id = $1",
e_retry.id,
)
.execute(engine.pool())
.await?;
engine.run_once().await?;
tokio::time::sleep(Duration::from_millis(100)).await;
let status_after_retry = engine.event(e_retry.id).await?.unwrap();
println!(" After retry: status={:?}\n", status_after_retry.status);
println!("[7] Running worker to deliver main events...");
let delivered = engine.run_once().await?;
println!(" Processed {delivered} events.");
tokio::time::sleep(Duration::from_millis(200)).await;
println!("\n[8] Events received by axum server:");
let received = log.0.lock().unwrap().clone();
for m in &received {
println!("{m}");
}
println!("\n[9] Delivery log from Postgres:");
for (label, id) in [("order.created", e1.id), ("payment.succeeded", e2.id), ("order.shipped", e3.id)] {
let attempts = engine.delivery_log(id).await?;
for a in &attempts {
println!(" {label}: {} in {}ms",
if a.success { "✓ delivered" } else { "✗ failed" },
a.duration_ms.unwrap_or(0),
);
}
}
let retry_log = engine.delivery_log(e_retry.id).await?;
println!(" retry.demo: {} attempt(s)", retry_log.len());
for a in &retry_log {
println!(" → attempt: {} ({}ms)", if a.success { "✓" } else { "✗" }, a.duration_ms.unwrap_or(0));
}
sqlx::query!("DELETE FROM webhook_endpoints WHERE id IN ($1, $2)", endpoint.id, dead_endpoint.id)
.execute(engine.pool())
.await?;
println!("\n╔══════════════════════════════════════╗");
println!("║ done ✓ ║");
println!("╚══════════════════════════════════════╝\n");
Ok(())
}