Skip to content

Installation

1. Install

bash
npm install @nestarc/webhook @nestjs/schedule @prisma/client

@nestjs/schedule and @prisma/client are peer dependencies.

2. Run the SQL Migration

The webhook tables are not managed through schema.prisma. They use raw SQL shipped with the package:

bash
# Apply with psql
psql "$DATABASE_URL" -f node_modules/@nestarc/webhook/src/sql/create-webhook-tables.sql

This creates three tables (webhook_endpoints, webhook_events, webhook_deliveries) with indexes. The migration is idempotent (IF NOT EXISTS).

It also runs CREATE EXTENSION IF NOT EXISTS pgcrypto for PostgreSQL < 13 compatibility.

View the full SQL
sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE IF NOT EXISTS webhook_endpoints (
  id                   UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  url                  VARCHAR(2048) NOT NULL,
  secret               VARCHAR(255)  NOT NULL,
  events               VARCHAR(255)[] NOT NULL DEFAULT '{}',
  active               BOOLEAN       NOT NULL DEFAULT TRUE,
  description          VARCHAR(500),
  metadata             JSONB,
  tenant_id            VARCHAR(255),
  consecutive_failures INT           NOT NULL DEFAULT 0,
  disabled_at          TIMESTAMPTZ,
  disabled_reason      VARCHAR(255),
  created_at           TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
  updated_at           TIMESTAMPTZ   NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS webhook_events (
  id          UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type  VARCHAR(255) NOT NULL,
  payload     JSONB        NOT NULL,
  tenant_id   VARCHAR(255),
  created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS webhook_deliveries (
  id              UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  event_id        UUID         NOT NULL REFERENCES webhook_events(id),
  endpoint_id     UUID         NOT NULL REFERENCES webhook_endpoints(id),
  status          VARCHAR(20)  NOT NULL DEFAULT 'PENDING',
  attempts        INT          NOT NULL DEFAULT 0,
  max_attempts    INT          NOT NULL DEFAULT 5,
  next_attempt_at TIMESTAMPTZ,
  claimed_at      TIMESTAMPTZ,
  last_attempt_at TIMESTAMPTZ,
  completed_at    TIMESTAMPTZ,
  response_status INT,
  response_body   TEXT,
  latency_ms      INT,
  last_error      TEXT
);

3. Register the Module

typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { WebhookModule } from '@nestarc/webhook';

@Module({
  imports: [
    WebhookModule.forRoot({
      prisma: prismaService,
      delivery: {
        timeout: 10_000,
        maxRetries: 5,
        backoff: 'exponential',
        jitter: true,
      },
      circuitBreaker: {
        failureThreshold: 5,
        cooldownMinutes: 60,
      },
      polling: {
        interval: 5000,
        batchSize: 50,
      },
    }),
  ],
})
export class AppModule {}
typescript
import { ConfigModule, ConfigService } from '@nestjs/config';
import { WebhookModule } from '@nestarc/webhook';

@Module({
  imports: [
    WebhookModule.forRootAsync({
      imports: [PrismaModule, ConfigModule],
      inject: [PrismaService, ConfigService],
      useFactory: (prisma: PrismaService, config: ConfigService) => ({
        prisma,
        delivery: {
          maxRetries: config.get('WEBHOOK_MAX_RETRIES', 5),
          timeout: config.get('WEBHOOK_TIMEOUT', 10_000),
        },
        circuitBreaker: {
          failureThreshold: config.get('WEBHOOK_CB_THRESHOLD', 5),
          cooldownMinutes: config.get('WEBHOOK_CB_COOLDOWN', 60),
        },
        polling: {
          interval: config.get('WEBHOOK_POLL_INTERVAL', 5000),
          batchSize: config.get('WEBHOOK_BATCH_SIZE', 50),
        },
      }),
    }),
  ],
})
export class AppModule {}

4. Send Your First Event

typescript
import { WebhookEvent } from '@nestarc/webhook';

export class OrderCreatedEvent extends WebhookEvent {
  static readonly eventType = 'order.created';

  constructor(
    public readonly orderId: string,
    public readonly total: number,
  ) {
    super();
  }
}
typescript
import { Injectable } from '@nestjs/common';
import { WebhookService } from '@nestarc/webhook';

@Injectable()
export class OrdersService {
  constructor(private readonly webhooks: WebhookService) {}

  async createOrder(dto: CreateOrderDto) {
    const order = await this.saveOrder(dto);
    await this.webhooks.send(new OrderCreatedEvent(order.id, order.total));
    return order;
  }
}

Module Options

OptionTypeDefaultDescription
prismainstancerequiredPrismaClient instance (optional if all custom repos provided)
delivery.timeoutnumber10000HTTP request timeout in ms
delivery.maxRetriesnumber5Maximum delivery attempts
delivery.backoff'exponential''exponential'Backoff strategy
delivery.jitterbooleantrueAdd ±10% random jitter to retry delays
circuitBreaker.failureThresholdnumber5Consecutive failures before disabling endpoint
circuitBreaker.cooldownMinutesnumber60Minutes before attempting recovery
polling.intervalnumber5000Delivery worker poll interval in ms
polling.batchSizenumber50Max deliveries per poll cycle
polling.staleSendingMinutesnumber5Minutes before a stuck SENDING delivery is recovered
allowPrivateUrlsbooleanfalseAllow private/internal URLs (dev/test only)

Released under the MIT License.