Skip to content

Latest commit

Β 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Convex Webhooks Component

npm version

A managed webhook delivery system for sending events to user-defined URLs. Handles queuing, retries with backoff, and cryptographic signing so recipients can verify authenticity. Supports both serialized and parallel delivery modes.

✨ Key Features

  • Queue Management: Outbound webhooks with configurable rate limits per destination URL
  • Automatic Retries: Exponential backoff for 5xx errors and network failures
  • Delivery Modes: Serialized (in-order) or parallel (out-of-order on failure)
  • Cryptographic Signing: Generate and store ed25519 key pairs per destination
  • Payload Verification: Sign payloads with private key, expose public key for recipient verification
  • Status Tracking: Track delivery status: pending, delivered, failed, retrying
  • Delivery History: Store attempts with timestamp, response code, and error details
  • URL Validation: Optional HEAD request to confirm reachability on registration
  • Retry Configuration: Configurable max retry attempts and retry window
  • Query Interface: Inspect delivery history and failed webhooks

πŸ“‹ Requirements Status

βœ… Core Requirements

  • Queue outbound webhooks with configurable rate limits per destination URL
  • Automatic retries with exponential backoff for 5xx errors and network failures
  • Configurable delivery modes: serialized (in-order) or parallel (out-of-order on failure)
  • Generate and store ed25519 key pairs per destination for request signing
  • Sign payloads with private key, expose public key for recipient verification
  • Track delivery status per webhook: pending, delivered, failed, retrying
  • Store delivery attempts with timestamp, response code, and error details
  • Validate destination URLs on registration (optional HEAD request to confirm reachability)
  • Configurable max retry attempts and retry window before marking as permanently failed
  • Expose query interface for delivery history and failed webhook inspection

βœ… Implementation Status

  • Event Management: Core event storage and retrieval system
  • Workpool Integration: Background processing infrastructure
  • Component Architecture: Modular Convex component design
  • Type Safety: Full TypeScript type definitions
  • Schema Design: Optimized database schema for webhook delivery

πŸš€ Basic Setup

This component uses Workpool for background processing. Install both packages:

npm install @convex-dev/webhooks @convex-dev/workpool
// convex/convex.config.ts
import { defineApp } from "convex/server";
import webhooks from "@convex-dev/webhooks/convex.config";

const app = defineApp();
app.use(webhooks);
export default app;
// convex/webhooks.ts (or your convex folder)
import { components } from "./_generated/api";
import { webhooks } from "@convex-dev/webhooks";

const webhookSystem = new webhooks(components.webhooks, {
  rateLimits: {
    perDestination: 100, // requests per minute
    global: 1000, // global requests per minute
  },
  delivery: {
    mode: "serialized", // or "parallel"
    maxRetries: 5,
    retryWindow: 3600, // 1 hour in seconds
  },
  signing: {
    algorithm: "ed25519",
    keyRotation: 86400, // 24 hours in seconds
  },
});

πŸ“€ Register Webhook Destination

export const registerWebhook = action({
  args: {
    url: v.string(),
    events: v.array(v.string()),
    config: v.object({
      mode: v.union(v.literal("serialized"), v.literal("parallel")),
      rateLimit: v.optional(v.number()),
      secret: v.optional(v.string()),
    }),
  },
  handler: async (ctx, { url, events, config }) => {
    // Validate destination URL
    const isValid = await webhookSystem.validateDestination(ctx, { url });
    if (!isValid) {
      throw new Error("Destination URL is not reachable");
    }

    // Generate signing key pair
    const keyPair = await webhookSystem.generateKeyPair(ctx, { url });
    
    // Register webhook destination
    const destination = await webhookSystem.registerDestination(ctx, {
      url,
      events,
      config,
      publicKey: keyPair.publicKey,
    });

    return { destination, publicKey: keyPair.publicKey };
  },
});

πŸ“€ Send Webhook

export const sendWebhook = action({
  args: {
    destinationId: v.id("destinations"),
    payload: v.any(),
    headers: v.optional(v.record(v.string(), v.string())),
  },
  handler: async (ctx, { destinationId, payload, headers }) => {
    const delivery = await webhookSystem.send(ctx, {
      destinationId,
      payload,
      headers,
      priority: "normal", // or "high"
    });

    return {
      deliveryId: delivery.id,
      status: delivery.status,
      estimatedDelivery: delivery.estimatedAt,
    };
  },
});

πŸ” Query Delivery Status

export const getDeliveryStatus = query({
  args: {
    deliveryId: v.id("deliveries"),
  },
  handler: async (ctx, { deliveryId }) => {
    const delivery = await webhookSystem.getDelivery(ctx, { deliveryId });
    const attempts = await webhookSystem.getDeliveryAttempts(ctx, { deliveryId });

    return {
      delivery: {
        id: delivery._id,
        status: delivery.status,
        createdAt: delivery._creationTime,
        attempts: delivery.attempts,
        lastAttempt: delivery.lastAttempt,
      },
      attempts: attempts.map(attempt => ({
        timestamp: attempt.timestamp,
        responseCode: attempt.responseCode,
        error: attempt.error,
        duration: attempt.duration,
      })),
    };
  },
});

πŸ“Š Delivery History

export const getDeliveryHistory = query({
  args: {
    destinationId: v.id("destinations"),
    status: v.optional(v.union(
      v.literal("pending"),
      v.literal("delivered"),
      v.literal("failed"),
      v.literal("retrying")
    )),
    paginationOpts: v.optional(paginationOptsValidator),
  },
  handler: async (ctx, { destinationId, status, paginationOpts }) => {
    const deliveries = await webhookSystem.listDeliveries(ctx, {
      destinationId,
      status,
      paginationOpts: paginationOpts || { cursor: null, numItems: 50 },
    });

    return deliveries;
  },
});

πŸ” Payload Verification

Recipients can verify webhook authenticity using the public key:

import { createVerify } from 'crypto';

export function verifyWebhook(payload: string, signature: string, publicKey: string): boolean {
  const verify = createVerify('SHA256');
  verify.update(payload);
  verify.end();
  
  return verify.verify(publicKey, signature, 'base64');
}

⚑ Advanced Configuration

const webhookSystem = new webhooks(components.webhooks, {
  rateLimits: {
    perDestination: {
      requests: 100,
      window: 60, // 1 minute
      burst: 10, // allow burst of 10
    },
    global: {
      requests: 1000,
      window: 60,
    },
  },
  delivery: {
    mode: "parallel",
    maxRetries: 7,
    retryWindow: 7200, // 2 hours
    backoffMultiplier: 2,
    initialDelay: 1000, // 1 second
  },
  signing: {
    algorithm: "ed25519",
    keyRotation: 86400, // 24 hours
    headerName: "X-Webhook-Signature",
  },
  validation: {
    timeout: 5000, // 5 seconds
    userAgent: "Convex-Webhooks/1.0",
    headers: {
      "Content-Type": "application/json",
    },
  },
});

πŸ”„ Event Processing with Workpool

The webhook system uses Workpool for reliable background processing. Workpool provides prioritized queues, concurrency limits, and built-in retries. Enqueue delivery actions through Workpool:

import { Workpool } from "@convex-dev/workpool";
import { components } from "./_generated/api";
import { internal } from "./_generated/api";

const deliveryPool = new Workpool(components.webhooks.workpool, {
  maxParallelism: 20,
  retryActionsByDefault: true,
  defaultRetryBehavior: { maxAttempts: 5, initialBackoffMs: 1000, base: 2 },
});

export const sendWebhook = mutation({
  args: {
    destinationId: v.id("destinations"),
    payload: v.any(),
  },
  handler: async (ctx, { destinationId, payload }) => {
    const delivery = await webhookSystem.createDelivery(ctx, {
      destinationId,
      payload,
    });

    await deliveryPool.enqueueAction(ctx, internal.webhooks.processDelivery, {
      deliveryId: delivery._id,
    });

    return { deliveryId: delivery._id };
  },
});

// The action that performs the actual HTTP delivery
export const processDelivery = internalAction({
  args: { deliveryId: v.id("deliveries") },
  handler: async (ctx, { deliveryId }) => {
    const delivery = await ctx.runQuery(internal.webhooks.getDelivery, { deliveryId });
    if (!delivery) return;

    const response = await fetch(delivery.url, {
      method: "POST",
      headers: { "Content-Type": "application/json", ...delivery.headers },
      body: JSON.stringify(delivery.payload),
    });

    await ctx.runMutation(internal.webhooks.recordAttempt, {
      deliveryId,
      responseCode: response.status,
      success: response.ok,
    });

    if (response.ok) {
      await ctx.runMutation(internal.webhooks.markDelivered, { deliveryId });
    } else if (response.status >= 500) {
      await ctx.runMutation(internal.webhooks.scheduleRetry, { deliveryId });
    } else {
      await ctx.runMutation(internal.webhooks.markFailed, { deliveryId });
    }
  },
});

For priority routing (high vs normal), configure multiple Workpool instances in your convex.config.ts and route to the appropriate pool.

πŸ“ˆ Monitoring & Analytics

export const getWebhookStats = query({
  args: {
    destinationId: v.id("destinations"),
    timeRange: v.object({
      start: v.number(),
      end: v.number(),
    }),
  },
  handler: async (ctx, { destinationId, timeRange }) => {
    const stats = await webhookSystem.getStats(ctx, {
      destinationId,
      timeRange,
    });

    return {
      totalDeliveries: stats.total,
      successfulDeliveries: stats.successful,
      failedDeliveries: stats.failed,
      averageLatency: stats.averageLatency,
      deliveryRate: stats.deliveryRate,
      errorRate: stats.errorRate,
    };
  },
});

πŸ› οΈ Development

# Install dependencies (Workpool is a peer dependency)
npm install @convex-dev/webhooks @convex-dev/workpool

# Run development server
npm run dev

# Run tests
npm test

πŸ“ License

MIT License - see LICENSE file for details.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages