Skip to content

Closes #675 Implemented Webhooks Engine - #691

Open
p3ris0n wants to merge 7 commits into
soroventures:mainfrom
p3ris0n:feat/webhooks-retry-engine
Open

Closes #675 Implemented Webhooks Engine#691
p3ris0n wants to merge 7 commits into
soroventures:mainfrom
p3ris0n:feat/webhooks-retry-engine

Conversation

@p3ris0n

@p3ris0n p3ris0n commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Feature: Webhook Engine with BullMQ and Exponential Backoff

Closes #675

Summary
Refactored the webhook dispatch system to use a robust, queue-based architecture powered by BullMQ. This update ensures reliable webhook delivery by implementing exponential backoff for failed requests and introducing a Dead-Letter Queue (DLQ) for dispatches that exhaust their retry attempts.

Key Changes
Database & Models
WebhookDelivery Model: Added a new schema to track individual webhook dispatches, including their payload, number of attempts, status (PENDING, SUCCESS, FAILED, DLQ), and detailed error logs.
Webhook Model: Fixed a schema bug caused by duplicate enum declarations in the events field.
Architecture & Queueing
BullMQ Integration (server/services/webhook-queue.js):
Initialized webhookQueue connected to the existing Redis instance.
Built a dedicated worker that fetches pending WebhookDelivery records, executes the HTTP request, and updates the delivery status.
Configured automatic exponential backoff (up to 5 attempts, starting with a 5-minute interval).
Added listeners to catch fully exhausted jobs and update their delivery status to DLQ.
Service Refactoring (server/services/webhook-service.js):
Replaced the naive synchronous retry loop in the dispatch function. It now asynchronously creates a WebhookDelivery record and pushes a job to BullMQ, ensuring the server doesn't block on network latency.
Server Initialization (server/index.js): Added a hook to initialize the worker on application startup.
Dashboard Features
Added GET /api/webhooks/:id/deliveries to fetch a webhook's delivery history.
Added POST /api/webhooks/deliveries/:deliveryId/retry to allow manual requeueing of FAILED or DLQ deliveries.
Testing
Added server/tests/webhook-engine.test.js to simulate webhook dispatches, verify database record creation, queue insertions, and test the worker's success and failure routines.
Type of Change
Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
Breaking change (fix or feature that would cause existing functionality to not work as expected)
This change requires a documentation update
Testing Instructions
Ensure your local .env has a valid REDIS_URL configured.
Run npm install in the server directory.
Run npx jest server/tests/webhook-engine.test.js to verify functionality.
Trigger a webhook locally and observe the background worker processing the job and updating the MongoDB document.

* @returns {array} 200 - Array of webhook deliveries
* @returns {object} 404 - Webhook not found
*/
router.get('/webhooks/:id/deliveries', authenticate, asyncHandler(async (req, res) => {
Comment on lines +104 to +124
router.get('/webhooks/:id/deliveries', authenticate, asyncHandler(async (req, res) => {
const webhook = await Webhook.findOne({
_id: req.params.id,
ownerPublicKey: req.user.publicKey,
});

if (!webhook) throw new AppError('Webhook not found', 404, 'NOT_FOUND');

const { status, limit = 50, skip = 0 } = req.query;
const query = { webhookId: webhook._id };
if (status) {
query.status = status;
}

const deliveries = await WebhookDelivery.find(query)
.sort({ createdAt: -1 })
.skip(Number(skip))
.limit(Number(limit));

res.json({ success: true, data: deliveries });
}));
query.status = status;
}

const deliveries = await WebhookDelivery.find(query)
* @returns {object} 200 - Success confirmation
* @returns {object} 404 - Delivery not found or not in DLQ
*/
router.post('/webhooks/deliveries/:deliveryId/retry', authenticate, asyncHandler(async (req, res) => {
Comment on lines +137 to +161
router.post('/webhooks/deliveries/:deliveryId/retry', authenticate, asyncHandler(async (req, res) => {
const delivery = await WebhookDelivery.findById(req.params.deliveryId).populate('webhookId');
if (!delivery) throw new AppError('WebhookDelivery not found', 404, 'NOT_FOUND');

if (delivery.webhookId.ownerPublicKey !== req.user.publicKey) {
throw new AppError('Unauthorized access to WebhookDelivery', 403, 'FORBIDDEN');
}

if (delivery.status !== 'DLQ' && delivery.status !== 'FAILED') {
throw new AppError('Only FAILED or DLQ deliveries can be manually retried', 400, 'BAD_REQUEST');
}

const { webhookQueue } = require('../services/webhook-queue');

// Reset status to PENDING and attempts to 0 for a fresh retry schedule
delivery.status = 'PENDING';
delivery.attempts = 0;
await delivery.save();

await webhookQueue.add('webhookDelivery', {
deliveryId: String(delivery._id),
});

res.json({ success: true, message: 'Delivery queued for retry' });
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(infrastructure): Build webhook retry engine with exponential backoff and dead-letter queue

2 participants