Skip to main content
Technology & EngineeringSecurity Ratelimit383 lines

Svix

Webhook delivery infrastructure including sending webhooks, retry logic, signature verification, event types, consumer portal, and message logging with Svix

Quick Summary28 lines
Svix treats webhooks as infrastructure, not application code. Instead of building your own retry queues, signature schemes, and delivery dashboards, you push messages to Svix and it handles delivery, retries, and observability. Your application defines event types and sends messages; Svix manages endpoints, verifies delivery, retries failures with exponential backoff, and provides a consumer-facing portal where your customers manage their own webhook subscriptions. This separation means you focus on what events to send, not how to deliver them reliably.

## Key Points

- Always set `eventId` on messages for idempotency. If your application retries the send (e.g., after a crash), Svix deduplicates by this ID and the consumer only receives it once.
- Define event type schemas upfront. This documents your webhook contract and enables Svix to validate payloads before delivery.
- Use the consumer portal to let customers manage their own endpoints. This eliminates support tickets for URL changes and event type subscriptions.
- Use `filterTypes` on endpoints so consumers only receive events they care about, reducing noise and unnecessary delivery attempts.
- Verify webhook signatures on the receiving side with the `Webhook` class. Never process unverified payloads.
- Monitor delivery attempts via the API or dashboard. Set up alerts for endpoints with sustained failures.
- Use `application.uid` matching your internal tenant ID so you do not need a separate mapping table.
- **Skipping signature verification on the receiving end.** Without verification, any party can forge webhook deliveries to your endpoint.
- **Sending webhooks synchronously in the request path.** Push the message to Svix and return immediately. Do not wait for delivery confirmation in the user-facing request.
- **Using generic event type names like "update".** Use dot-separated, specific names (`invoice.paid`, `user.created`) so consumers can filter precisely.

## Quick Example

```typescript
npm install svix
```

```typescript
// .env.local
SVIX_API_KEY=sk_yourSvixKey
SVIX_WEBHOOK_SECRET=whsec_yourSigningSecret
```
skilldb get security-ratelimit-skills/SvixFull skill: 383 lines
Paste into your CLAUDE.md or agent config

Svix: Webhook Delivery Infrastructure

Core Philosophy

Svix treats webhooks as infrastructure, not application code. Instead of building your own retry queues, signature schemes, and delivery dashboards, you push messages to Svix and it handles delivery, retries, and observability. Your application defines event types and sends messages; Svix manages endpoints, verifies delivery, retries failures with exponential backoff, and provides a consumer-facing portal where your customers manage their own webhook subscriptions. This separation means you focus on what events to send, not how to deliver them reliably.

Setup

Installation

npm install svix

Environment Configuration

// .env.local
SVIX_API_KEY=sk_yourSvixKey
SVIX_WEBHOOK_SECRET=whsec_yourSigningSecret

Client Initialization

// lib/svix.ts
import { Svix } from "svix";

export const svix = new Svix(process.env.SVIX_API_KEY!);

Key Techniques

Defining Event Types

// lib/svix-setup.ts
import { svix } from "./svix";

export async function registerEventTypes() {
  const eventTypes = [
    {
      name: "invoice.paid",
      description: "An invoice has been paid successfully",
      schemas: {
        "1": {
          type: "object",
          properties: {
            invoiceId: { type: "string" },
            amount: { type: "number" },
            currency: { type: "string" },
            paidAt: { type: "string", format: "date-time" },
          },
          required: ["invoiceId", "amount", "currency", "paidAt"],
        },
      },
    },
    {
      name: "user.created",
      description: "A new user account was created",
      schemas: {
        "1": {
          type: "object",
          properties: {
            userId: { type: "string" },
            email: { type: "string" },
            createdAt: { type: "string", format: "date-time" },
          },
          required: ["userId", "email", "createdAt"],
        },
      },
    },
  ];

  for (const eventType of eventTypes) {
    await svix.eventType.create(eventType);
  }
}

Creating Applications (Tenants)

// lib/svix-apps.ts
import { svix } from "./svix";

export async function createWebhookApp(tenantId: string, tenantName: string) {
  const app = await svix.application.create({
    uid: tenantId,
    name: tenantName,
  });

  return app;
}

export async function getOrCreateApp(tenantId: string, tenantName: string) {
  try {
    return await svix.application.get(tenantId);
  } catch {
    return await svix.application.create({
      uid: tenantId,
      name: tenantName,
    });
  }
}

Sending Webhook Messages

// app/api/webhooks/send/route.ts
import { svix } from "@/lib/svix";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { tenantId, eventType, payload } = await req.json();

  try {
    const message = await svix.message.create(tenantId, {
      eventType,
      payload: {
        type: eventType,
        data: payload,
        timestamp: new Date().toISOString(),
      },
    });

    return NextResponse.json({ messageId: message.id });
  } catch (error) {
    console.error("Failed to send webhook:", error);
    return NextResponse.json(
      { error: "Failed to queue webhook" },
      { status: 500 }
    );
  }
}

Sending Webhooks from Business Logic

// lib/webhook-events.ts
import { svix } from "./svix";

export async function emitInvoicePaid(
  tenantId: string,
  invoiceId: string,
  amount: number,
  currency: string
) {
  await svix.message.create(tenantId, {
    eventType: "invoice.paid",
    eventId: `inv_${invoiceId}_${Date.now()}`,
    payload: {
      type: "invoice.paid",
      data: {
        invoiceId,
        amount,
        currency,
        paidAt: new Date().toISOString(),
      },
    },
  });
}

export async function emitUserCreated(
  tenantId: string,
  userId: string,
  email: string
) {
  await svix.message.create(tenantId, {
    eventType: "user.created",
    eventId: `usr_${userId}_${Date.now()}`,
    payload: {
      type: "user.created",
      data: {
        userId,
        email,
        createdAt: new Date().toISOString(),
      },
    },
  });
}

Managing Endpoints

// lib/svix-endpoints.ts
import { svix } from "./svix";

export async function addEndpoint(
  tenantId: string,
  url: string,
  eventTypes: string[]
) {
  const endpoint = await svix.endpoint.create(tenantId, {
    url,
    filterTypes: eventTypes,
    description: `Webhook endpoint for ${url}`,
  });

  return endpoint;
}

export async function listEndpoints(tenantId: string) {
  const endpoints = await svix.endpoint.list(tenantId);
  return endpoints.data;
}

export async function deleteEndpoint(tenantId: string, endpointId: string) {
  await svix.endpoint.delete(tenantId, endpointId);
}

Signature Verification (Receiving Webhooks)

// app/api/webhooks/receive/route.ts
import { Webhook } from "svix";
import { NextRequest, NextResponse } from "next/server";

const wh = new Webhook(process.env.SVIX_WEBHOOK_SECRET!);

export async function POST(req: NextRequest) {
  const body = await req.text();

  const headers = {
    "svix-id": req.headers.get("svix-id") ?? "",
    "svix-timestamp": req.headers.get("svix-timestamp") ?? "",
    "svix-signature": req.headers.get("svix-signature") ?? "",
  };

  let payload: Record<string, unknown>;

  try {
    payload = wh.verify(body, headers) as Record<string, unknown>;
  } catch (err) {
    console.error("Webhook signature verification failed:", err);
    return NextResponse.json(
      { error: "Invalid webhook signature" },
      { status: 400 }
    );
  }

  // Process the verified webhook
  const eventType = payload.type as string;

  switch (eventType) {
    case "invoice.paid":
      await handleInvoicePaid(payload.data as Record<string, unknown>);
      break;
    case "user.created":
      await handleUserCreated(payload.data as Record<string, unknown>);
      break;
    default:
      console.log("Unhandled event type:", eventType);
  }

  return NextResponse.json({ received: true });
}

async function handleInvoicePaid(data: Record<string, unknown>) {
  console.log("Invoice paid:", data.invoiceId);
}

async function handleUserCreated(data: Record<string, unknown>) {
  console.log("User created:", data.userId);
}

Consumer Portal (Self-Service)

// app/api/webhooks/portal/route.ts
import { svix } from "@/lib/svix";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { tenantId } = await req.json();

  // Generate a short-lived portal URL for the tenant
  const dashboard = await svix.authentication.appPortalAccess(tenantId, {});

  return NextResponse.json({
    url: dashboard.url,
    token: dashboard.token,
  });
}

Message Logging and Retry

// lib/svix-monitoring.ts
import { svix } from "./svix";

export async function listMessages(tenantId: string) {
  const messages = await svix.message.list(tenantId, {
    limit: 50,
  });

  return messages.data.map((msg) => ({
    id: msg.id,
    eventType: msg.eventType,
    timestamp: msg.timestamp,
    payload: msg.payload,
  }));
}

export async function getMessageAttempts(tenantId: string, messageId: string) {
  const attempts = await svix.messageAttempt.listByMsg(tenantId, messageId);

  return attempts.data.map((attempt) => ({
    id: attempt.id,
    status: attempt.status,
    responseStatusCode: attempt.responseStatusCode,
    timestamp: attempt.timestamp,
    url: attempt.url,
  }));
}

export async function retryMessage(tenantId: string, messageId: string) {
  // Svix handles retries automatically, but you can trigger a manual retry
  await svix.messageAttempt.resend(tenantId, messageId);
}

export async function retryFailedForEndpoint(
  tenantId: string,
  endpointId: string
) {
  await svix.endpoint.recover(tenantId, endpointId, {});
}

Idempotent Message Sending

// Using eventId for idempotency
import { svix } from "./svix";

export async function sendIdempotentWebhook(
  tenantId: string,
  eventType: string,
  eventId: string,
  data: Record<string, unknown>
) {
  // If this eventId was already sent, Svix will deduplicate
  await svix.message.create(tenantId, {
    eventType,
    eventId, // acts as idempotency key
    payload: {
      type: eventType,
      data,
    },
  });
}

Best Practices

  • Always set eventId on messages for idempotency. If your application retries the send (e.g., after a crash), Svix deduplicates by this ID and the consumer only receives it once.
  • Define event type schemas upfront. This documents your webhook contract and enables Svix to validate payloads before delivery.
  • Use the consumer portal to let customers manage their own endpoints. This eliminates support tickets for URL changes and event type subscriptions.
  • Use filterTypes on endpoints so consumers only receive events they care about, reducing noise and unnecessary delivery attempts.
  • Verify webhook signatures on the receiving side with the Webhook class. Never process unverified payloads.
  • Monitor delivery attempts via the API or dashboard. Set up alerts for endpoints with sustained failures.
  • Use application.uid matching your internal tenant ID so you do not need a separate mapping table.

Anti-Patterns

  • Building your own retry queue. Svix provides exponential backoff retries out of the box. Rolling your own duplicates infrastructure and introduces subtle bugs around retry timing and deduplication.
  • Skipping signature verification on the receiving end. Without verification, any party can forge webhook deliveries to your endpoint.
  • Sending webhooks synchronously in the request path. Push the message to Svix and return immediately. Do not wait for delivery confirmation in the user-facing request.
  • Using generic event type names like "update". Use dot-separated, specific names (invoice.paid, user.created) so consumers can filter precisely.
  • Ignoring failed deliveries. If an endpoint is consistently failing, investigate or disable it. Svix will eventually disable endpoints after repeated failures, but proactive monitoring avoids data loss.
  • Putting sensitive data in webhook payloads without encryption. Webhooks are delivered over HTTPS, but the consumer's endpoint security is outside your control. Send IDs and let the consumer fetch sensitive details via your API.

Install this skill directly: skilldb add security-ratelimit-skills

Get CLI access →