Webhooks
Webhooks are user-defined HTTP callbacks that are triggered by specific events in a source system and sent to a destination URL. In VibeReference applications, webhooks provide a powerful way to integrate with external services and build event-driven architectures.
Introduction to Webhooks
At their core, webhooks implement a simple concept:
- A source system monitors for specific events (e.g., payment processed, form submitted)
- When an event occurs, the source system sends an HTTP request to a pre-configured URL
- The destination system receives the request, processes the data, and typically returns a 2xx response
- The communication is asynchronous and initiated by the source system
Webhooks enable real-time integration, eliminate polling, and support a decoupled architecture.
Common Webhook Use Cases in VibeReference
| Source | Event | Webhook Purpose |
|---|---|---|
| Stripe | payment.succeeded |
Update subscription status in database |
| Supabase | Record created/updated | Trigger external workflows |
| GitHub | push, pull_request |
Trigger CI/CD pipeline |
| Form submission | Form completion | Process lead data, send confirmation email |
| Email service | Email opened/clicked | Update user engagement metrics |
| Calendar service | Appointment booked | Notify team, update availability |
Receiving Webhooks in Next.js
Basic Webhook Endpoint
Create a route handler in Next.js to receive webhook requests:
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = headers().get('stripe-signature') || '';
try {
// Verify webhook signature
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent;
await handleSuccessfulPayment(paymentIntent);
break;
case 'customer.subscription.updated':
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
// Handle other event types
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Return a 200 response to acknowledge receipt of the event
return new NextResponse(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (err: any) {
console.error(`Webhook error: ${err.message}`);
return new NextResponse(JSON.stringify({ error: err.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
}
async function handleSuccessfulPayment(paymentIntent: Stripe.PaymentIntent) {
// Implementation for handling successful payment
console.log(`Processing payment: ${paymentIntent.id}`);
// Update database, send confirmation emails, etc.
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
// Implementation for handling subscription updates
console.log(`Subscription updated: ${subscription.id}`);
// Update user subscription status in database
}
Webhook Security
To secure your webhook endpoints:
- Verify signatures: Most webhook providers include a signature header
- Use HTTPS: Always use HTTPS for webhook endpoints
- Keep secrets secure: Store webhook secrets in environment variables
- Implement idempotency: Process webhooks idempotently to handle duplicates
- IP filtering: Optionally restrict to known IP ranges of the webhook provider
Supabase Webhook Integration
Supabase provides built-in webhook functionality through Database Webhooks:
Setting Up a Supabase Database Webhook
-- Create a webhook when a new user profile is created
create or replace function public.handle_new_user()
returns trigger as $$
begin
perform net.http_post(
'https://your-vibereference-app.vercel.app/api/webhooks/new-user',
json_build_object('user_id', new.id, 'created_at', new.created_at)::text,
'application/json'
);
return new;
end;
$$ language plpgsql security definer;
-- Create trigger on insert to profiles table
create trigger on_user_created
after insert on public.profiles
for each row execute procedure public.handle_new_user();
Receiving Supabase Webhooks
// app/api/webhooks/new-user/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { headers } from 'next/headers';
// Note: Prefer server component or client for Supabase connection
// This is just an example of webhook handling
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(req: NextRequest) {
// Get the webhookKey from environment variables
const webhookKey = process.env.WEBHOOK_KEY;
// Get the authorization header
const authHeader = headers().get('authorization');
// Verify the webhook key
if (authHeader !== `Bearer ${webhookKey}`) {
return new NextResponse(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
try {
const payload = await req.json();
const { user_id, created_at } = payload;
// Process the new user
// e.g., send welcome email, set up default data, etc.
await setupNewUserDefaults(user_id);
return new NextResponse(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Error processing new user webhook:', error);
return new NextResponse(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
}
async function setupNewUserDefaults(userId: string) {
// Example: Add default items for the new user
await supabaseAdmin
.from('user_settings')
.insert({
user_id: userId,
theme: 'light',
notifications_enabled: true
});
// Additional setup as needed
}
Sending Webhooks from Your VibeReference App
Your application can also be a webhook provider:
// lib/webhooks.ts
import crypto from 'crypto';
interface WebhookPayload {
event: string;
data: any;
timestamp: number;
}
export async function sendWebhook(
url: string,
event: string,
data: any,
secret: string
) {
// Create the payload
const payload: WebhookPayload = {
event,
data,
timestamp: Date.now()
};
// Create the signature
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Send the webhook
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: JSON.stringify(payload)
});
// Check if successful
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to send webhook: ${error}`);
}
return {
success: true,
statusCode: response.status
};
} catch (error) {
console.error('Error sending webhook:', error);
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
Testing Webhooks
Local Development
For local testing, you can use tools like:
-
ngrok: Exposes your local server to the internet
# Install ngrok npm install -g ngrok # Start your Next.js app npm run dev # Create a tunnel to your local server ngrok http 3000 -
Webhook simulators: Tools like Stripe CLI
# Install Stripe CLI brew install stripe/stripe-cli/stripe # Forward events to your local server stripe listen --forward-to http://localhost:3000/api/webhooks/stripe
Mock Webhook Sender
Create a tool to simulate webhook events:
// scripts/send-test-webhook.ts
import fetch from 'node-fetch';
import crypto from 'crypto';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const webhookUrl = 'http://localhost:3000/api/webhooks/test';
const secret = process.env.WEBHOOK_TEST_SECRET || 'test-secret';
async function sendTestWebhook() {
// Create test payload
const payload = {
event: 'test.event',
data: {
id: crypto.randomUUID(),
test: true,
message: 'This is a test webhook event'
},
timestamp: Date.now()
};
// Create signature
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: JSON.stringify(payload)
});
const result = await response.text();
console.log(`Status: ${response.status}`);
console.log(`Response: ${result}`);
} catch (error) {
console.error('Error sending test webhook:', error);
}
}
sendTestWebhook();
Webhook Best Practices
- Respond quickly: Process webhooks asynchronously and respond with 200 OK immediately
- Implement retry logic: Handle temporary failures gracefully
- Verify signatures: Always validate webhook authenticity
- Use idempotency keys: Process webhooks idempotently to handle duplicates
- Log webhook events: Maintain logs for debugging and auditing
- Set up monitoring: Alert on webhook failures
- Handle rate limits: Be prepared for bursts of webhook events
Resources
- Supabase Webhooks Documentation
- Stripe Webhooks Guide
- GitHub Webhooks
- ngrok Documentation
- Webhook.site - Testing and debugging webhooks