Svix
Webhook delivery infrastructure including sending webhooks, retry logic, signature verification, event types, consumer portal, and message logging with Svix
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 linesSvix: 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
eventIdon 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
filterTypeson endpoints so consumers only receive events they care about, reducing noise and unnecessary delivery attempts. - Verify webhook signatures on the receiving side with the
Webhookclass. Never process unverified payloads. - Monitor delivery attempts via the API or dashboard. Set up alerts for endpoints with sustained failures.
- Use
application.uidmatching 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
Related Skills
Arcjet
Rate limiting, bot detection, email validation, and shield attack protection using Arcjet with Next.js middleware and stacking rules
Cloudflare Turnstile
Privacy-preserving CAPTCHA alternative using Cloudflare Turnstile for bot protection with server-side verification in Next.js and Express
Security Headers
Security headers with Helmet.js, Content Security Policy, CORS configuration, CSRF protection, rate limiting patterns, and Next.js security headers
OWASP ZAP
Automated web application security testing, API scanning, and CI/CD DAST integration using OWASP ZAP
Snyk
Dependency vulnerability scanning, license compliance, and continuous security monitoring using Snyk CLI and CI/CD integrations
Unkey
API key management, rate limiting, usage tracking, key verification, temporary keys, ratelimit API, and analytics with Unkey