一个面向 SaaS 和内部平台的可靠 Webhook 基础设施,支持多租户、API Key 鉴权、幂等接收、持久化队列、HMAC 签名、指数退避重试、死信队列、限流和 Prometheus 指标。
业务系统直接同步调用外部 Webhook 时,容易受到目标服务超时、故障和流量波动影响。本项目把“事件接收”和“外部投递”拆成两个独立阶段:
业务系统 -> API 接收并持久化 -> 后台工作器 -> 外部 Webhook
API 只要成功写入数据库即可快速响应,投递失败由工作器异步重试。
- 多租户 API Key 隔离
Idempotency-Key唯一约束- SQLite WAL 持久化任务队列
- 数据库租约,支持多个工作器竞争任务
- 指数退避和随机抖动
- 达到重试上限后进入死信状态
- HMAC-SHA256 请求签名
- SSRF 防护,默认拒绝私有和保留网络
- 租户级固定窗口限流
- 请求体大小限制和超时控制
- Prometheus 文本指标
- Docker Compose 分离运行 API 和工作器
详细设计见 架构文档。
- Node.js 24+
- 或 Docker / Docker Compose
项目无第三方运行时依赖,使用 Node.js 内置 HTTP、SQLite 和测试模块。
npm test
npm run seed
npm start另开终端启动工作器:
npm run worker本地调试私有地址端点时设置:
$env:ALLOW_PRIVATE_TARGETS = "true"docker compose up --build生产环境必须修改 ADMIN_TOKEN,并通过反向代理配置 TLS。
curl.exe -X POST http://localhost:8788/admin/tenants `
-H "Authorization: Bearer local-admin-token" `
-H "Content-Type: application/json" `
-d '{"name":"订单平台"}'响应中的 apiKey 只应在创建时展示一次。
curl.exe -X POST http://localhost:8788/v1/endpoints `
-H "X-API-Key: whk_xxx" `
-H "Content-Type: application/json" `
-d '{"name":"订单回调","targetUrl":"https://example.com/webhooks/orders"}'响应中的 signingSecret 用于目标服务验签。
curl.exe -X POST http://localhost:8788/v1/endpoints/ENDPOINT_ID/events `
-H "X-API-Key: whk_xxx" `
-H "Idempotency-Key: order-1001-created" `
-H "Content-Type: application/json" `
-d '{"type":"order.created","payload":{"orderId":"1001","amount":19900}}'重复提交相同幂等键会返回已有事件,不会重复入队。
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/health |
健康检查 |
GET |
/metrics |
Prometheus 指标 |
POST |
/admin/tenants |
创建租户,需要管理员令牌 |
POST |
/v1/endpoints |
创建端点 |
GET |
/v1/endpoints |
查询端点 |
POST |
/v1/endpoints/:id/events |
幂等接收事件 |
GET |
/v1/events |
查询事件 |
GET |
/v1/events/:id |
查询事件详情 |
投递请求头:
x-webhook-id:事件 ID,目标端可用于消费幂等x-webhook-timestamp:Unix 秒时间戳x-webhook-signature:sha256=<HMAC>
签名原文:
timestamp.rawBody
| 环境变量 | 默认值 | 说明 |
|---|---|---|
PORT |
8788 |
API 端口 |
DATABASE_PATH |
data/webhook-hub.db |
SQLite 文件 |
ADMIN_TOKEN |
local-admin-token |
管理员令牌 |
WORKER_CONCURRENCY |
4 |
单次领取任务数 |
WORKER_POLL_MS |
500 |
空闲轮询间隔 |
WORKER_LEASE_MS |
30000 |
任务租约时间 |
DELIVERY_TIMEOUT_MS |
5000 |
单次投递超时 |
RATE_LIMIT_PER_MINUTE |
120 |
每租户每分钟请求数 |
MAX_PAYLOAD_BYTES |
262144 |
最大请求体 |
MAX_ATTEMPTS |
5 |
最大投递次数 |
ALLOW_PRIVATE_TARGETS |
false |
是否允许私有网络目标 |
npm test
npm run test:load压测脚本会向本地服务并发写入 500 个事件并输出吞吐结果。
- 当前版本采用 SQLite,强调零依赖、事务和可演示性,不适合直接承载跨区域大规模生产流量。
- 交付语义为至少一次,目标服务需要消费幂等。
- 限流为进程内固定窗口,多副本生产部署应替换为 Redis 或网关限流。
- 管理员令牌和签名密钥当前存储方式适合演示,生产环境应使用密钥管理服务。