Skip to content

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

bash
/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:

bash
/template add-webhook "Description"
# or
/t add-webhook "Description"

This creates:

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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

typescript
// 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

typescript
// 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:

bash
# Install ngrok
npm install -g ngrok

# Expose local server
ngrok http 3000

# Update webhook URL in service dashboard

Test Endpoints

Create test endpoints for development:

typescript
// 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

typescript
// 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.

Built with ❤️ for the AI Coding community, by Praney Behl