Add Persistent Webhook Emission to Existing App
Integrate configurable, signed webhooks that fire on key app events with reliable persistence and retry.
The route
6 steps to Done
- 01
Create webhook configuration model and migration
Persist webhook registrations in the database.
Preview prompt + verify gate ▾ Hide ▴
1. Write a new Sequelize migration file that creates a table named "Webhooks" with the following columns: - id: primary key, UUID, not null - url: STRING(2048), not null - event: STRING(100), not null (event name to listen for) - secret: STRING(256), not null (used for HMAC signature) - enabled: BOOLEAN, default true - created_at / updated_at: TIMESTAMP with default now Ensure the migration includes up/down functions and uses snake_case column names. 2. Create a Sequelize model file (e.g., models/webhook.js) that defines the model with the same columns, enables timestamps, and exports it. 3. The model should include a class method `isEnabled()` returning the boolean flag. 4. Do not modify any other files. Keep code ready to run with `npx sequelize db:migrate`.
- ✓ Can you see the 'Webhooks' table in the database after running migrations?
- ✓ Does the model file export a Sequelize model named Webhook?
- ✓ Are all required columns present with correct data types?
- ✓ Does the model include an `isEnabled()` class method?
- 02
Add REST API endpoint to register webhooks
Allow external services to create, enable, or disable webhook registrations via HTTP.
Preview prompt + verify gate ▾ Hide ▴
1. In the existing API router, add a POST endpoint at "/webhooks". - Apply `requireAuth` middleware. - Validate request body: `url` (required, must be a valid URL), `event` (required, non‑empty string), `secret` (required, at least 32 chars), `enabled` (optional boolean default true). - On validation failure, respond with 400 and error details. - On success, create a Webhook record using the Sequelize model from step 1. - Return HTTP 201 with JSON of the saved webhook (excluding the secret for security). 2. Export the router unchanged. Do not modify unrelated routes.
- ✓ Does POST /api/webhooks return 201 on a valid request?
- ✓ Is the request body validated and 400 returned on missing/invalid fields?
- ✓ Is the new webhook persisted in the database?
- ✓ Is the response JSON free of the secret field?
- 03
Implement webhook dispatch service with retries and signatures
Send signed JSON payloads to registered URLs and reliably retry on failure.
Preview prompt + verify gate ▾ Hide ▴
1. Create a new file `services/webhookDispatcher.js` exporting an async function `sendWebhooks(eventName, payload)`. - Query the Webhook model for records where `event = eventName` and `enabled = true`. - For each webhook, construct a JSON body containing `{event: eventName, data: payload}`. - Compute an HMAC‑SHA256 signature of the stringified body using the webhook's `secret`; set header `X-Webhook-Signature` to the hex digest. - Use `node-fetch` to POST to the webhook's `url` with `Content-Type: application/json` and the body. - Implement retry logic: on network error or non‑2xx response, wait 100ms * 2^attempt (up to 3 attempts). Log each attempt with `log.info`; on final failure, log with `log.error`. - Do not block the caller; fire-and-forget is acceptable, but the function should await all dispatches before resolving. 2. Export the function for use elsewhere. 3. Do NOT include UI code; only backend logic.
- ✓ Does the function query enabled webhooks for the given event?
- ✓ Is the HMAC‑SHA256 signature header correctly generated?
- ✓ Are failed deliveries retried up to 3 times with exponential backoff?
- ✓ Are all attempts logged, and final failures logged as errors?
- ✓ Does the function await all dispatches before resolving?
- 04
Integrate dispatch into a core event (user creation)
Trigger webhook emission when a new user signs up without affecting the main flow.
Preview prompt + verify gate ▾ Hide ▴
1. Open `controllers/authController.js` and locate the async `register` function that creates a new user. - After the `await User.create(...)` and before sending the HTTP response, import the dispatcher: `const { sendWebhooks } = require('../services/webhookDispatcher');` - Call `await sendWebhooks('user.created', { id: newUser.id, email: newUser.email });` inside a try/catch block. - If the dispatcher throws, log the error with `log.error` but do not modify the response to the client. 2. Ensure the file still exports the controller unchanged. 3. Do not add any UI changes.
- ✓ Does a POST to /api/auth/register trigger the webhook dispatcher?
- ✓ Is the payload sent to dispatch `{id, email}`?
- ✓ If the webhook fails, does the user registration still succeed?
- ✓ Is any dispatch error logged without breaking the response?
- 05
Provide sample payload endpoint for developers
Give a way to view the exact JSON payload that will be sent for each event.
Preview prompt + verify gate ▾ Hide ▴
1. In the API router, add a GET endpoint at `/webhooks/sample/:event`. - No auth required. - Retrieve `req.params.event`. - Build a sample payload object: `{ event: eventName, data: { message: "sample payload for " + eventName } }`. - Respond with status 200 and the JSON object. 2. Ensure the route is exported with existing router. 3. Do not modify any other routes.
- ✓ Can you GET /api/webhooks/sample/user.created and receive a JSON payload?
- ✓ Does the payload contain the correct `event` field matching the URL param?
- ✓ Is the response status 200?
- ✓ Is the route added without auth middleware?
- 06
Write integration tests for registration and dispatch
Verify end‑to‑end behavior of webhook registration, storage, and delivery.
Preview prompt + verify gate ▾ Hide ▴
1. In `tests/webhook.test.js`, write a test suite using `supertest` to: a. POST to `/api/webhooks` with a valid payload (url: `http://example.com/receive`, event: `user.created`, secret: `testsecret1234567890abcdef`, enabled: true). Assert response 201 and saved webhook ID. b. Use `nock('http://example.com')` to intercept POST `/receive` and capture request body and headers. Set it to reply 200. c. POST to `/api/auth/register` with a new user payload. d. Verify that nock received exactly one request, that the body matches `{event:'user.created', data:{id: expect.any(Number), email: expect.any(String)}}`, and that header `X-Webhook-Signature` equals HMAC‑SHA256 of the body using the secret. e. Ensure the user registration response is 200 and contains the user data. 2. Add a second test that forces a failure (e.g., nock returns 500) and asserts that the retry logic attempts three calls (use nock's `.times(3)` matcher) and ultimately logs an error but still returns 200 for registration. 3. All tests must pass (`npm test` returns 0 failures). 4. Do not include UI code; only backend test files.
- ✓ Do tests verify that POST /api/webhooks creates a DB entry?
- ✓ Do tests simulate the `user.created` event and assert an external POST was made with correct body and signature?
- ✓ Is the retry behavior tested by forcing a 500 response and checking three attempts?
- ✓ Do all Jest tests complete with 0 failures?