Skip to content
Flows
App Builder Generated Intermediate · 2-3 hours

Add Persistent Webhook Emission to Existing App

Integrate configurable, signed webhooks that fire on key app events with reliable persistence and retry.

Start Route · 6 steps

The route

6 steps to Done

  1. 01

    Create webhook configuration model and migration

    Persist webhook registrations in the database.

    Preview prompt + verify gate ▾

    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?
  2. 02

    Add REST API endpoint to register webhooks

    Allow external services to create, enable, or disable webhook registrations via HTTP.

    Preview prompt + verify gate ▾

    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?
  3. 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 ▾

    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?
  4. 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 ▾

    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?
  5. 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 ▾

    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?
  6. 06

    Write integration tests for registration and dispatch

    Verify end‑to‑end behavior of webhook registration, storage, and delivery.

    Preview prompt + verify gate ▾

    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?