add-webhook
Access: /template add-webhook or /t add-webhook
Creates webhook endpoints for handling external service integrations (Stripe, GitHub, etc.) with proper security, validation, and error handling.
What It Does
The add-webhook command helps you create:
- Secure webhook endpoints with signature verification
- Request validation and parsing
- Error handling and retry logic
- Event processing and database updates
- Proper logging and monitoring
Usage
/template add-webhook "Description"
# or
/t add-webhook "Description"When prompted, specify:
- Webhook source (Stripe, GitHub, custom)
- Events to handle
- Processing logic required
- Security requirements
Prerequisites
- A MakerKit project initialized with Orchestre
- External service credentials configured
- Commercial MakerKit license from MakerKit
What Gets Created
1. Webhook Route Handler
Located in apps/web/app/api/webhooks/[service]/route.ts:
- Signature verification
- Event parsing
- Business logic processing
- Error responses
2. Event Handlers
Located in apps/web/lib/webhooks/[service]/:
- Individual event type handlers
- Database operations
- Side effects (emails, notifications)
3. Configuration
- Environment variables for secrets
- Event type mappings
- Retry configuration
Example: Stripe Webhook
Creating a Stripe webhook for subscription events:
/template add-webhook "Description"
# or
/t add-webhook "Description"This creates:
// apps/web/app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getStripeInstance } from '@kit/stripe/client';
import { handleSubscriptionCreated, handleSubscriptionUpdated } from '@/lib/webhooks/stripe';
const stripe = getStripeInstance();
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(
body,
signature,
webhookSecret
);
} catch (error) {
console.error('Webhook signature verification failed:', error);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
try {
// Handle different event types
switch (event.type) {
case 'customer.subscription.created':
await handleSubscriptionCreated(event);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}Event Handler Example
// apps/web/lib/webhooks/stripe/subscription-handlers.ts
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createServerClient } from '@supabase/ssr';
export async function handleSubscriptionCreated(event: Stripe.Event) {
const subscription = event.data.object as Stripe.Subscription;
const client = getSupabaseServerClient({ admin: true });
// Update subscription in database
const { error } = await client
.from('subscriptions')
.upsert({
id: subscription.id,
account_id: subscription.metadata.account_id,
status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
cancel_at_period_end: subscription.cancel_at_period_end,
price_id: subscription.items.data[0].price.id,
quantity: subscription.items.data[0].quantity,
});
if (error) {
throw new Error(`Failed to update subscription: ${error.message}`);
}
// Send confirmation email
await sendSubscriptionConfirmation(subscription);
}Common Webhook Patterns
GitHub Webhook
// Verify GitHub signature
const signature = headers().get('x-hub-signature-256');
const isValid = verifyGitHubSignature(body, signature, secret);
// Handle events
switch (event.action) {
case 'opened':
await handlePullRequestOpened(event);
break;
case 'closed':
await handlePullRequestClosed(event);
break;
}Custom Webhook with API Key
// Verify API key
const apiKey = headers().get('x-api-key');
if (apiKey !== process.env.WEBHOOK_API_KEY) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}Security Best Practices
1. Signature Verification
Always verify webhook signatures:
// Generic signature verification
function verifySignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}2. Idempotency
Handle duplicate events:
// Store processed event IDs
const { data: existing } = await client
.from('webhook_events')
.select('id')
.eq('event_id', event.id)
.single();
if (existing) {
return NextResponse.json({ message: 'Event already processed' });
}
// Process event and store ID
await processEvent(event);
await client.from('webhook_events').insert({
event_id: event.id,
processed_at: new Date().toISOString(),
});3. Timeout Handling
Respond quickly and process asynchronously:
// Quick response
export async function POST(request: NextRequest) {
const event = await parseEvent(request);
// Queue for processing
await queueEvent(event);
// Respond immediately
return NextResponse.json({ received: true });
}Error Handling
Retry Logic
// Return appropriate status codes
if (temporaryError) {
// 503 tells the service to retry
return NextResponse.json(
{ error: 'Temporary failure' },
{ status: 503 }
);
}
if (permanentError) {
// 400 tells the service not to retry
return NextResponse.json(
{ error: 'Invalid request' },
{ status: 400 }
);
}Logging
// Structured logging
console.log({
event: 'webhook_received',
type: event.type,
id: event.id,
timestamp: new Date().toISOString(),
});
console.error({
event: 'webhook_error',
type: event.type,
error: error.message,
stack: error.stack,
});Testing Webhooks
Local Development
Use ngrok or similar for local testing:
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 3000
# Update webhook URL in service dashboardTest Endpoints
Create test endpoints for development:
// apps/web/app/api/webhooks/[service]/test/route.ts
export async function POST(request: NextRequest) {
if (process.env.NODE_ENV !== 'development') {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// Simulate webhook event
const testEvent = createTestEvent();
await processWebhook(testEvent);
return NextResponse.json({ success: true });
}Monitoring
Health Checks
// Track webhook health
await client.from('webhook_health').insert({
service: 'stripe',
event_type: event.type,
status: 'success',
processing_time_ms: Date.now() - startTime,
timestamp: new Date().toISOString(),
});Alerting
Set up alerts for:
- Failed signature verifications
- Processing errors
- Unusual event volumes
- High processing times
License Requirement
Important: This command requires a commercial MakerKit license from https://makerkit.dev?atp=MqaGgc MakerKit is a premium SaaS starter kit and requires proper licensing for commercial use.
