Volume 3: Human-System Collaboration

Pattern 24: Webhooks & Event Streaming

Part III: Integration Patterns


Opening Scenario: The Form That Sent No Notifications

Rachel submitted a customer complaint form on her company's website at 2:47 PM:

Customer Complaint Form

Customer: Sarah Martinez
Email: sarah@email.com
Order #: 38472
Issue: Product arrived damaged
Priority: High
Description: The laptop arrived with a cracked screen...

[Submit]

She clicked Submit. The page showed:

✓ Complaint Submitted

Ticket #: 2847
Our team will review your complaint.

Rachel expected someone would be notified. But nothing happened.

At 5:00 PM, the customer service team went home. No one knew about the complaint.

At 8:00 AM the next day, Sarah called: "I submitted a complaint 18 hours ago. Has ANYONE looked at it?"

The manager, Tom, checked the system. There it was - Ticket #2847, submitted yesterday, priority HIGH, completely ignored.

"Why wasn't I notified?" Tom asked the developer.

The developer, Mike, showed him the code:

// Bad: Form just saves to database
app.post('/api/complaints', async (req, res) => {
  const complaint = await db.insert('complaints', {
    customer: req.body.customer,
    email: req.body.email,
    orderNumber: req.body.orderNumber,
    priority: req.body.priority,
    description: req.body.description,
    submittedAt: new Date()
  });

  res.json({ 
    success: true, 
    ticketId: complaint.id 
  });

  // That's it. No notification.
  // No Slack message.
  // No email alert.
  // Complaint sits in database silently.
});

"The form doesn't notify anyone," Mike said. "It just saves the data."

"Can you make it send notifications?" Tom asked.

"Sure, I can add that."


Mike updated the code:

// Better: Send notification
app.post('/api/complaints', async (req, res) => {
  const complaint = await db.insert('complaints', req.body);

  // Send email to support team
  await sendEmail({
    to: 'support@company.com',
    subject: `New ${req.body.priority} Priority Complaint`,
    body: `Complaint #${complaint.id} from ${req.body.customer}`
  });

  // Send Slack notification
  await sendSlackMessage({
    channel: '#customer-support',
    text: `🚨 New ${req.body.priority} priority complaint from ${req.body.customer}`
  });

  res.json({ success: true, ticketId: complaint.id });
});

Now when complaints arrived, the support team got notified.

But Tom had more requirements:

"Can you also: - Create a ticket in Zendesk - Add a row to our Google Sheet - Post to our internal dashboard - Send SMS for HIGH priority - Update our metrics in Datadog - Log to our audit system"

Mike groaned. "That's six more integrations. The code will be a mess."


The architect, Lisa, saw the problem. "You're tightly coupling the form to every notification system. What if we used webhooks instead?"

She showed Mike a different approach:

class WebhookSystem {
  constructor() {
    this.subscribers = new Map();
  }

  async subscribe(event, webhookURL, config) {
    if (!this.subscribers.has(event)) {
      this.subscribers.set(event, []);
    }

    this.subscribers.get(event).push({
      url: webhookURL,
      secret: config.secret,
      filter: config.filter || null,
      retryPolicy: config.retryPolicy || { maxRetries: 3 },
      active: true
    });
  }

  async emit(event, data) {
    const subscribers = this.subscribers.get(event) || [];

    // Notify all subscribers asynchronously
    const notifications = subscribers
      .filter(sub => this.matchesFilter(sub.filter, data))
      .map(sub => this.notify(sub, event, data));

    // Don't wait for webhooks (fire and forget)
    Promise.allSettled(notifications).catch(err => {
      console.error('Webhook notification error:', err);
    });
  }

  async notify(subscriber, event, data) {
    const payload = {
      event: event,
      timestamp: new Date().toISOString(),
      data: data
    };

    const signature = this.generateSignature(payload, subscriber.secret);

    try {
      const response = await fetch(subscriber.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': signature,
          'X-Webhook-Event': event
        },
        body: JSON.stringify(payload),
        timeout: 5000
      });

      if (!response.ok) {
        throw new Error(`Webhook failed: ${response.status}`);
      }

      this.logSuccess(subscriber, event);

    } catch (error) {
      this.handleError(subscriber, event, error);
    }
  }
}

Now the complaint form was simple:

app.post('/api/complaints', async (req, res) => {
  const complaint = await db.insert('complaints', req.body);

  // Emit event - webhooks handle the rest
  await webhooks.emit('complaint.created', {
    id: complaint.id,
    customer: complaint.customer,
    priority: complaint.priority,
    orderNumber: complaint.orderNumber
  });

  res.json({ success: true, ticketId: complaint.id });
});

Each system subscribed to the webhook:

// Email notification
webhooks.subscribe('complaint.created', 'https://email-service.com/webhooks', {
  secret: EMAIL_WEBHOOK_SECRET
});

// Slack notification  
webhooks.subscribe('complaint.created', 'https://hooks.slack.com/services/...', {
  filter: { priority: 'HIGH' } // Only high priority
});

// Zendesk integration
webhooks.subscribe('complaint.created', 'https://api.zendesk.com/webhooks', {
  secret: ZENDESK_SECRET
});

// Google Sheets
webhooks.subscribe('complaint.created', 'https://sheets-api.com/append', {
  secret: SHEETS_SECRET
});

// SMS for HIGH priority
webhooks.subscribe('complaint.created', 'https://twilio.com/webhook', {
  filter: { priority: 'HIGH' },
  secret: TWILIO_SECRET
});

The form didn't know about any integration. It just said "something happened."

Like a fishing line with multiple hooks - when a fish bites, all the hooks know about it. 🎣

Each system could: - Subscribe independently - Unsubscribe without code changes - Filter events (only HIGH priority) - Retry on failure - Verify authenticity (signature)

When a complaint came in, the webhook system notified everyone who cared. The form just fished - the hooks did the work.

Smart fish (secure systems) validated the signature before trusting the hook.

Context

Webhooks & Event Streaming applies when:

External systems need to know: When forms submit, update, delete, change state

Real-time response required: Immediate notification, not batch processing

Loose coupling desired: Form shouldn't know about all integrations

Multiple subscribers exist: Many systems care about same events

Integration flexibility needed: Add/remove subscribers without code changes

Asynchronous processing: Don't block form submission waiting for integrations

Event-driven architecture: Systems react to events rather than poll

Third-party integrations: Zapier, IFTTT, Slack, Teams, custom systems

Problem Statement

Most systems tightly couple forms to notification/integration logic:

Hardcoded integrations:

// Bad: Form knows about every integration
async function handleFormSubmit(data) {
  await saveToDatabase(data);
  await sendEmail(data);
  await postToSlack(data);
  await createZendeskTicket(data);
  await updateGoogleSheet(data);
  await sendSMS(data);

  // Adding new integration = changing this code
  // Removing integration = changing this code
  // Form is tightly coupled to everything
}

Synchronous blocking:

// Bad: Wait for all integrations before responding
app.post('/submit', async (req, res) => {
  await db.save(req.body);

  await sendEmail(req.body);      // Blocks for 2 seconds
  await postToSlack(req.body);    // Blocks for 1 second
  await createTicket(req.body);   // Blocks for 3 seconds

  // User waits 6+ seconds for response!
  res.json({ success: true });
});

No retry logic:

// Bad: Integration fails, data lost
try {
  await sendNotification(data);
} catch (error) {
  // Notification lost forever
  console.error('Failed to notify:', error);
}

No event filtering:

// Bad: Everyone gets everything
function notifyAll(data) {
  // Slack doesn't care about LOW priority
  // But gets notified anyway
  // Noise, alert fatigue
  sendToSlack(data);
  sendToEmail(data);
  sendToSMS(data);
}

No security:

// Bad: Anyone can POST to webhook endpoint
app.post('/webhook', (req, res) => {
  // No signature verification
  // No authentication
  // Accept any POST request
  processWebhook(req.body);
});

No monitoring:

// Bad: Webhooks fail silently
await fetch(webhookURL, { 
  method: 'POST',
  body: JSON.stringify(data)
});

// Did it work? Unknown.
// Did it fail? Unknown.
// Should we retry? Unknown.

We need decoupled, asynchronous, reliable webhook system with filtering, security, retry, and monitoring.

Forces

Loose Coupling vs Control

  • Webhooks decouple systems
  • But less visibility into what happens
  • Balance independence with oversight

Async vs Consistency

  • Async webhooks are fast
  • But eventual consistency, not immediate
  • Balance speed with guarantees

Retry vs Duplicate

  • Retrying ensures delivery
  • But may cause duplicate events
  • Balance reliability with idempotency

Open vs Secure

  • Open webhooks enable integration
  • But expose attack surface
  • Balance accessibility with security

Simplicity vs Reliability

  • Simple webhooks are easy
  • But reliability needs complexity
  • Balance ease with robustness

Solution

Implement comprehensive webhook system that allows external systems to subscribe to form events, delivers notifications asynchronously with retry logic, provides event filtering and transformation, ensures security through signature verification, monitors delivery success, and enables real-time event streaming for complex workflows.

The pattern has seven key strategies:

1. Webhook Registration & Management

Allow systems to subscribe to events:

class WebhookRegistry {
  constructor(database) {
    this.db = database;
  }

  async registerWebhook(config) {
    // Validate webhook configuration
    this.validateConfig(config);

    // Generate secret for signature verification
    const secret = this.generateSecret();

    // Store webhook
    const webhook = await this.db.insert('webhooks', {
      name: config.name,
      url: config.url,
      events: JSON.stringify(config.events),
      filters: JSON.stringify(config.filters || {}),
      secret: secret,
      active: true,
      createdAt: new Date(),
      createdBy: config.userId
    });

    // Test webhook connection
    await this.testWebhook(webhook);

    return {
      id: webhook.id,
      secret: secret // Return once, securely
    };
  }

  async updateWebhook(webhookId, updates) {
    await this.db.update('webhooks', webhookId, {
      ...updates,
      updatedAt: new Date()
    });
  }

  async deleteWebhook(webhookId) {
    await this.db.update('webhooks', webhookId, {
      active: false,
      deletedAt: new Date()
    });
  }

  async getWebhooksForEvent(eventName) {
    const webhooks = await this.db.query(`
      SELECT * FROM webhooks
      WHERE active = true
      AND JSON_CONTAINS(events, '"${eventName}"')
    `);

    return webhooks;
  }

  async testWebhook(webhook) {
    // Send test payload
    const testPayload = {
      event: 'webhook.test',
      timestamp: new Date().toISOString(),
      data: { test: true }
    };

    try {
      const response = await fetch(webhook.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': this.sign(testPayload, webhook.secret),
          'X-Webhook-Event': 'webhook.test'
        },
        body: JSON.stringify(testPayload),
        timeout: 10000
      });

      if (!response.ok) {
        throw new Error(`Test failed: ${response.status}`);
      }

      return { success: true };

    } catch (error) {
      throw new Error(`Webhook test failed: ${error.message}`);
    }
  }

  validateConfig(config) {
    if (!config.name || config.name.length < 3) {
      throw new Error('Webhook name required (min 3 characters)');
    }

    if (!config.url || !this.isValidURL(config.url)) {
      throw new Error('Valid webhook URL required');
    }

    if (!config.events || config.events.length === 0) {
      throw new Error('At least one event required');
    }

    // Validate URL is HTTPS in production
    if (process.env.NODE_ENV === 'production' && 
        !config.url.startsWith('https://')) {
      throw new Error('HTTPS required for webhook URL');
    }
  }

  generateSecret() {
    const crypto = require('crypto');
    return crypto.randomBytes(32).toString('hex');
  }

  sign(payload, secret) {
    const crypto = require('crypto');
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(JSON.stringify(payload));
    return hmac.digest('hex');
  }
}

2. Event Emission & Delivery

Emit events and deliver to subscribers:

class WebhookDelivery {
  constructor(registry, queue) {
    this.registry = registry;
    this.queue = queue;
  }

  async emit(eventName, eventData, options = {}) {
    // Get all webhooks subscribed to this event
    const webhooks = await this.registry.getWebhooksForEvent(eventName);

    // Filter webhooks based on event data
    const matchingWebhooks = webhooks.filter(webhook => 
      this.matchesFilter(webhook.filters, eventData)
    );

    // Queue delivery for each webhook
    const deliveries = matchingWebhooks.map(webhook => 
      this.queueDelivery(webhook, eventName, eventData, options)
    );

    return {
      event: eventName,
      webhooksNotified: matchingWebhooks.length,
      deliveryIds: await Promise.all(deliveries)
    };
  }

  async queueDelivery(webhook, eventName, eventData, options) {
    const delivery = {
      id: this.generateDeliveryId(),
      webhookId: webhook.id,
      webhookUrl: webhook.url,
      event: eventName,
      payload: this.buildPayload(eventName, eventData),
      secret: webhook.secret,
      attempt: 0,
      maxAttempts: options.maxRetries || 3,
      status: 'pending',
      queuedAt: new Date()
    };

    // Add to delivery queue
    await this.queue.enqueue('webhook-delivery', delivery, {
      priority: this.getPriority(eventName),
      delay: 0
    });

    return delivery.id;
  }

  async deliverWebhook(delivery) {
    delivery.attempt++;
    delivery.lastAttemptAt = new Date();

    try {
      const signature = this.sign(delivery.payload, delivery.secret);

      const response = await fetch(delivery.webhookUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': signature,
          'X-Webhook-Event': delivery.event,
          'X-Webhook-Delivery': delivery.id,
          'X-Webhook-Attempt': delivery.attempt.toString()
        },
        body: JSON.stringify(delivery.payload),
        timeout: 10000
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      // Success
      delivery.status = 'delivered';
      delivery.deliveredAt = new Date();
      delivery.responseStatus = response.status;

      await this.logDelivery(delivery);

      return { success: true };

    } catch (error) {
      delivery.status = 'failed';
      delivery.error = error.message;

      // Retry?
      if (delivery.attempt < delivery.maxAttempts) {
        delivery.status = 'retrying';

        // Exponential backoff
        const delay = Math.pow(2, delivery.attempt) * 1000; // 2s, 4s, 8s

        await this.queue.enqueue('webhook-delivery', delivery, {
          delay: delay
        });
      } else {
        // Max retries exceeded
        delivery.status = 'failed-permanently';
        await this.logDelivery(delivery);
        await this.alertWebhookFailure(delivery);
      }

      return { success: false, error: error.message };
    }
  }

  matchesFilter(filters, data) {
    if (!filters || Object.keys(filters).length === 0) {
      return true; // No filter = match all
    }

    return Object.entries(filters).every(([key, value]) => {
      if (Array.isArray(value)) {
        return value.includes(data[key]);
      }
      return data[key] === value;
    });
  }

  buildPayload(eventName, eventData) {
    return {
      event: eventName,
      timestamp: new Date().toISOString(),
      data: eventData
    };
  }

  sign(payload, secret) {
    const crypto = require('crypto');
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(JSON.stringify(payload));
    return hmac.digest('hex');
  }
}

3. Signature Verification

Secure webhooks with signature verification:

class WebhookSecurity {
  static verifySignature(payload, signature, secret) {
    const expectedSignature = this.computeSignature(payload, secret);

    // Constant-time comparison to prevent timing attacks
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  }

  static computeSignature(payload, secret) {
    const crypto = require('crypto');
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(typeof payload === 'string' ? payload : JSON.stringify(payload));
    return hmac.digest('hex');
  }

  static middleware(secret) {
    return (req, res, next) => {
      const signature = req.headers['x-webhook-signature'];

      if (!signature) {
        return res.status(401).json({
          error: 'Missing webhook signature'
        });
      }

      const payload = req.body;

      if (!WebhookSecurity.verifySignature(payload, signature, secret)) {
        return res.status(401).json({
          error: 'Invalid webhook signature'
        });
      }

      next();
    };
  }
}

// Webhook receiver validates signature
app.post('/webhooks/complaints', 
  WebhookSecurity.middleware(WEBHOOK_SECRET),
  async (req, res) => {
    // Signature verified - process webhook
    const event = req.body;

    if (event.event === 'complaint.created') {
      await handleNewComplaint(event.data);
    }

    // Acknowledge receipt
    res.json({ received: true });
  }
);

4. Event Filtering & Transformation

Filter and transform events before delivery:

class WebhookFiltering {
  constructor() {
    this.filters = new Map();
    this.transformers = new Map();
  }

  defineFilter(webhookId, filterConfig) {
    this.filters.set(webhookId, filterConfig);
  }

  defineTransformer(webhookId, transformerFn) {
    this.transformers.set(webhookId, transformerFn);
  }

  shouldDeliver(webhook, event, data) {
    const filter = this.filters.get(webhook.id);

    if (!filter) {
      return true; // No filter = deliver all
    }

    return this.evaluateFilter(filter, data);
  }

  evaluateFilter(filter, data) {
    // Simple field-based filtering
    if (filter.field && filter.value) {
      const fieldValue = this.getNestedValue(data, filter.field);

      if (filter.operator === 'equals') {
        return fieldValue === filter.value;
      } else if (filter.operator === 'contains') {
        return Array.isArray(filter.value) 
          ? filter.value.includes(fieldValue)
          : fieldValue?.includes(filter.value);
      } else if (filter.operator === 'greater_than') {
        return fieldValue > filter.value;
      } else if (filter.operator === 'less_than') {
        return fieldValue < filter.value;
      }
    }

    // Complex filtering
    if (filter.expression) {
      return this.evaluateExpression(filter.expression, data);
    }

    return true;
  }

  transformPayload(webhook, payload) {
    const transformer = this.transformers.get(webhook.id);

    if (!transformer) {
      return payload; // No transformation
    }

    return transformer(payload);
  }

  getNestedValue(obj, path) {
    return path.split('.').reduce((current, key) => 
      current?.[key], obj
    );
  }
}

// Example: Only notify Slack for HIGH priority
webhookFiltering.defineFilter(slackWebhookId, {
  field: 'priority',
  operator: 'equals',
  value: 'HIGH'
});

// Example: Transform payload for third-party service
webhookFiltering.defineTransformer(zendeskWebhookId, (payload) => {
  return {
    ticket: {
      subject: `Complaint from ${payload.data.customer}`,
      description: payload.data.description,
      priority: payload.data.priority.toLowerCase(),
      custom_fields: [
        { id: 360000123456, value: payload.data.orderNumber }
      ]
    }
  };
});

5. Retry Logic with Exponential Backoff

Reliably deliver webhooks with intelligent retry:

class WebhookRetry {
  constructor(config = {}) {
    this.maxRetries = config.maxRetries || 5;
    this.initialDelay = config.initialDelay || 1000; // 1 second
    this.maxDelay = config.maxDelay || 3600000; // 1 hour
    this.backoffMultiplier = config.backoffMultiplier || 2;
  }

  async deliverWithRetry(delivery) {
    let attempt = 0;
    let lastError = null;

    while (attempt < this.maxRetries) {
      try {
        const result = await this.attemptDelivery(delivery, attempt);

        // Success
        return {
          success: true,
          attempt: attempt + 1,
          result: result
        };

      } catch (error) {
        lastError = error;
        attempt++;

        if (attempt < this.maxRetries) {
          const delay = this.calculateDelay(attempt);
          console.log(`Webhook delivery failed (attempt ${attempt}), retrying in ${delay}ms`);
          await this.sleep(delay);
        }
      }
    }

    // All retries exhausted
    return {
      success: false,
      attempts: this.maxRetries,
      error: lastError.message
    };
  }

  calculateDelay(attempt) {
    // Exponential backoff: 1s, 2s, 4s, 8s, 16s, ...
    const delay = this.initialDelay * Math.pow(this.backoffMultiplier, attempt - 1);

    // Cap at max delay
    return Math.min(delay, this.maxDelay);
  }

  async attemptDelivery(delivery, attempt) {
    const response = await fetch(delivery.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': delivery.signature,
        'X-Webhook-Delivery-Id': delivery.id,
        'X-Webhook-Attempt': (attempt + 1).toString()
      },
      body: JSON.stringify(delivery.payload),
      timeout: 10000
    });

    if (!response.ok) {
      // Check if error is retryable
      if (this.isRetryableError(response.status)) {
        throw new Error(`HTTP ${response.status} (retryable)`);
      } else {
        throw new Error(`HTTP ${response.status} (permanent failure)`);
      }
    }

    return response;
  }

  isRetryableError(statusCode) {
    // Retry on server errors and rate limiting
    return statusCode >= 500 || statusCode === 429;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

6. Webhook Monitoring & Analytics

Track webhook delivery success and failures:

class WebhookMonitoring {
  constructor(database) {
    this.db = database;
  }

  async logDelivery(delivery) {
    await this.db.insert('webhook_deliveries', {
      webhookId: delivery.webhookId,
      deliveryId: delivery.id,
      event: delivery.event,
      status: delivery.status,
      attempts: delivery.attempt,
      queuedAt: delivery.queuedAt,
      deliveredAt: delivery.deliveredAt,
      duration: delivery.deliveredAt 
        ? delivery.deliveredAt - delivery.queuedAt 
        : null,
      responseStatus: delivery.responseStatus,
      error: delivery.error
    });
  }

  async getWebhookStats(webhookId, timeRange) {
    const stats = await this.db.query(`
      SELECT 
        COUNT(*) as total,
        SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as successful,
        SUM(CASE WHEN status LIKE 'failed%' THEN 1 ELSE 0 END) as failed,
        AVG(CASE WHEN status = 'delivered' THEN attempts ELSE NULL END) as avgAttempts,
        AVG(CASE WHEN status = 'delivered' THEN duration ELSE NULL END) as avgDuration
      FROM webhook_deliveries
      WHERE webhookId = ?
        AND queuedAt >= ?
        AND queuedAt <= ?
    `, [webhookId, timeRange.start, timeRange.end]);

    return {
      total: stats[0].total,
      successful: stats[0].successful,
      failed: stats[0].failed,
      successRate: stats[0].total > 0 
        ? (stats[0].successful / stats[0].total) * 100 
        : 0,
      avgAttempts: stats[0].avgAttempts || 0,
      avgDuration: stats[0].avgDuration || 0
    };
  }

  async getFailedDeliveries(webhookId, limit = 100) {
    return await this.db.query(`
      SELECT *
      FROM webhook_deliveries
      WHERE webhookId = ?
        AND status LIKE 'failed%'
      ORDER BY queuedAt DESC
      LIMIT ?
    `, [webhookId, limit]);
  }

  async generateHealthReport(webhookId) {
    const last24h = {
      start: new Date(Date.now() - 24 * 60 * 60 * 1000),
      end: new Date()
    };

    const stats = await this.getWebhookStats(webhookId, last24h);
    const recentFailures = await this.getFailedDeliveries(webhookId, 10);

    const health = this.calculateHealth(stats);

    return {
      webhookId: webhookId,
      period: '24 hours',
      health: health, // healthy, degraded, unhealthy
      stats: stats,
      recentFailures: recentFailures,
      recommendations: this.generateRecommendations(stats, recentFailures)
    };
  }

  calculateHealth(stats) {
    if (stats.total === 0) {
      return 'unknown';
    }

    if (stats.successRate >= 95) {
      return 'healthy';
    } else if (stats.successRate >= 80) {
      return 'degraded';
    } else {
      return 'unhealthy';
    }
  }

  generateRecommendations(stats, failures) {
    const recommendations = [];

    if (stats.successRate < 80) {
      recommendations.push('Success rate below 80% - investigate failures');
    }

    if (stats.avgAttempts > 2) {
      recommendations.push('High retry rate - check endpoint reliability');
    }

    if (stats.avgDuration > 5000) {
      recommendations.push('Slow response times - optimize endpoint');
    }

    return recommendations;
  }
}

7. Event Streaming for Real-Time Updates

Stream events in real-time using Server-Sent Events or WebSockets:

class EventStream {
  constructor() {
    this.clients = new Map();
  }

  setupSSE(app) {
    app.get('/api/events/stream', (req, res) => {
      // Setup SSE
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');

      const clientId = this.generateClientId();
      const filter = req.query.events?.split(',') || null;

      // Register client
      this.clients.set(clientId, {
        response: res,
        filter: filter,
        connectedAt: new Date()
      });

      // Send initial connection event
      this.sendEvent(res, 'connected', { clientId });

      // Cleanup on disconnect
      req.on('close', () => {
        this.clients.delete(clientId);
      });
    });
  }

  broadcast(event, data) {
    this.clients.forEach((client, clientId) => {
      // Check if client subscribed to this event
      if (client.filter && !client.filter.includes(event)) {
        return;
      }

      this.sendEvent(client.response, event, data);
    });
  }

  sendEvent(response, event, data) {
    response.write(`event: ${event}\n`);
    response.write(`data: ${JSON.stringify(data)}\n\n`);
  }

  generateClientId() {
    return Math.random().toString(36).substring(7);
  }
}

// Usage: Stream complaint events to dashboard
eventStream.setupSSE(app);

// When complaint created
webhooks.on('complaint.created', (data) => {
  eventStream.broadcast('complaint.created', data);
});

// Client receives real-time updates
const eventSource = new EventSource('/api/events/stream?events=complaint.created');

eventSource.addEventListener('complaint.created', (e) => {
  const complaint = JSON.parse(e.data);
  // Update dashboard in real-time
  updateDashboard(complaint);
});

Implementation Details

Complete Webhook System

class CompleteWebhookSystem {
  constructor(database, queue) {
    this.registry = new WebhookRegistry(database);
    this.delivery = new WebhookDelivery(this.registry, queue);
    this.security = new WebhookSecurity();
    this.filtering = new WebhookFiltering();
    this.retry = new WebhookRetry();
    this.monitoring = new WebhookMonitoring(database);
    this.stream = new EventStream();
  }

  // Register new webhook
  async register(config) {
    return await this.registry.registerWebhook(config);
  }

  // Emit event to all subscribers
  async emit(event, data) {
    // Deliver via webhooks
    await this.delivery.emit(event, data);

    // Broadcast to SSE clients
    this.stream.broadcast(event, data);

    return { event, broadcasted: true };
  }

  // Process webhook from queue
  async processDelivery(delivery) {
    return await this.retry.deliverWithRetry(delivery);
  }

  // Get webhook health
  async getHealth(webhookId) {
    return await this.monitoring.generateHealthReport(webhookId);
  }
}

// Usage
const webhooks = new CompleteWebhookSystem(database, queue);

// Business registers webhook
const slack = await webhooks.register({
  name: 'Slack Notifications',
  url: 'https://hooks.slack.com/services/...',
  events: ['complaint.created', 'complaint.resolved'],
  filters: {
    priority: ['HIGH', 'URGENT']
  },
  userId: currentUser.id
});

// Application emits events
app.post('/api/complaints', async (req, res) => {
  const complaint = await db.insert('complaints', req.body);

  // Emit event - webhooks handle notification
  await webhooks.emit('complaint.created', {
    id: complaint.id,
    customer: complaint.customer,
    priority: complaint.priority,
    createdAt: complaint.createdAt
  });

  res.json({ success: true, id: complaint.id });
});

// Monitor webhook health
const health = await webhooks.getHealth(slack.id);
console.log(`Slack webhook: ${health.health} (${health.stats.successRate}% success)`);

Consequences

Benefits

Loose Coupling: - Form doesn't know about integrations - Add/remove subscribers easily - Independent evolution

Asynchronous: - Don't block form submission - Fast response to users - Background processing

Reliable: - Retry on failure - Exponential backoff - Delivery guarantees

Flexible: - Filter events - Transform payloads - Subscribe to specific events

Secure: - Signature verification - HTTPS required - Prevent spoofing

Observable: - Monitor delivery - Track failures - Health metrics

Scalable: - Queue-based delivery - Parallel processing - Handle high volume

Liabilities

Eventual Consistency: - Not immediate - May be delayed - Order not guaranteed

Complexity: - More moving parts - Queue management - Monitoring required

Debugging Harder: - Asynchronous flow - Multiple systems - Distributed tracing needed

Security Risks: - Expose endpoints - Signature verification critical - Must validate everything

Webhook Hell: - Too many webhooks - Difficult to track - Governance needed

Domain Examples

Customer Support

webhooks.emit('ticket.created', {
  id: ticket.id,
  customer: ticket.customer,
  priority: ticket.priority
});

E-commerce

webhooks.emit('order.placed', {
  orderId: order.id,
  total: order.total,
  items: order.items
});

SaaS Applications

webhooks.emit('user.signup', {
  userId: user.id,
  email: user.email,
  plan: user.plan
});

Payments

webhooks.emit('payment.succeeded', {
  paymentId: payment.id,
  amount: payment.amount,
  customer: payment.customer
});

Prerequisites: - Volume 3, Pattern 21: External Data Integration (webhooks integrate systems) - Volume 3, Pattern 23: API-Driven Rules (rules trigger webhooks)

Synergies: - Volume 3, Pattern 18: Audit Trail (log webhook deliveries) - Volume 3, Pattern 20: Scheduled Actions (scheduled webhooks) - All patterns (webhooks notify about all events)

Conflicts: - Synchronous workflows - Immediate consistency required - Offline-first applications

Alternatives: - Polling (pull vs push) - Message queues (internal only) - Direct API calls (synchronous)

Known Uses

Stripe: Payment webhooks

GitHub: Repository event webhooks

Slack: Incoming webhooks

Twilio: SMS delivery webhooks

Shopify: Order webhooks

Zapier: Webhook triggers

Discord: Bot webhooks

SendGrid: Email event webhooks


Further Reading

Academic Foundations

  • Event-Driven Architecture: Richards, M., & Ford, N. (2020). Fundamentals of Software Architecture. O'Reilly. ISBN: 978-1492043454 - Chapter on event-driven architecture
  • Webhooks Design: Jenkov, J. (2014). "Webhook Tutorial." http://tutorials.jenkov.com/webhooks/index.html
  • Publish-Subscribe: Eugster, P.T., et al. (2003). "The many faces of publish/subscribe." ACM Computing Surveys 35(2): 114-131.

Practical Implementation

Standards & Security

  • Pattern 22: State-Aware Behavior - State changes trigger webhooks
  • Pattern 23: Audit Trail - Log webhook deliveries
  • Pattern 25: Scheduled Actions - Scheduled vs event-driven
  • Pattern 26: External Data Integration - Webhooks as integration
  • Volume 2, Chapter 1: The Universal Event Log - Event foundation
  • Volume 1, Chapter 8: Architecture of Domain-Specific Systems - Event patterns

Tools & Services

Implementation Examples