FeaturesBlogDocs GitHub Get Started

Webhooks and Event-Driven Architecture with flashQ

Webhooks are the backbone of modern integrations. When Stripe processes a payment, GitHub receives a push, or Shopify gets an order, they send webhooks to your application. The challenge? Handling them reliably at scale.

flashQ transforms webhook handling from a fragile synchronous process into a robust, event-driven architecture.

The Webhook Problem

Typical webhook issues:

  • Timeouts: Webhook providers expect fast responses (2-30 seconds)
  • Retries: Providers retry failed webhooks, causing duplicate processing
  • Order: Events can arrive out of order
  • Scale: Traffic spikes during peak times

The flashQ Solution

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Webhook    β”‚     β”‚    flashQ    β”‚     β”‚   Worker     β”‚
β”‚   Provider   │────▢│   (Enqueue)  │────▢│  (Process)   β”‚
β”‚              β”‚     β”‚   < 100ms    β”‚     β”‚   Async      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚
       β”‚   Fast response    β”‚   Reliable processing
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Handling Stripe Webhooks

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { Queue } from 'flashq';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const queue = new Queue('stripe-webhooks', { useHttp: true });

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  // Verify webhook signature
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Enqueue for async processing (fast response to Stripe)
  await queue.add(event.type, event, {
    jobId: event.id,  // Idempotency - prevents duplicate processing
  });

  // Respond immediately (< 100ms)
  return NextResponse.json({ received: true });
}

Stripe Worker

// workers/stripe.ts
import { Worker } from 'flashq';

new Worker('stripe-webhooks', async (job) => {
  const event = job.data;

  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;

    case 'invoice.paid':
      await handleInvoicePaid(event.data.object);
      break;

    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object);
      break;

    default:
      console.log(`Unhandled event: ${event.type}`);
  }
}, {
  concurrency: 10,
});

async function handleCheckoutComplete(session) {
  // Create user account
  await db.users.create({
    email: session.customer_email,
    stripeCustomerId: session.customer,
  });

  // Send welcome email
  await emailQueue.add('welcome', {
    to: session.customer_email,
  });
}

GitHub Webhooks

// app/api/webhooks/github/route.ts
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('x-hub-signature-256');
  const event = request.headers.get('x-github-event');
  const deliveryId = request.headers.get('x-github-delivery');

  // Verify signature
  const expected = `sha256=${crypto
    .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!)
    .update(body)
    .digest('hex')}`;

  if (signature !== expected) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  // Enqueue
  await queue.add(event!, JSON.parse(body), {
    jobId: deliveryId!,  // Idempotency
  });

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

Sending Outgoing Webhooks

flashQ can also send webhooks to your users:

// Send webhook when job completes
worker.on('completed', async (job, result) => {
  if (job.data.webhookUrl) {
    await webhookQueue.add('send', {
      url: job.data.webhookUrl,
      event: 'job.completed',
      payload: { jobId: job.id, result },
    }, {
      attempts: 5,
      backoff: { type: 'exponential', delay: 1000 },
    });
  }
});

// Webhook sender worker
new Worker('outgoing-webhooks', async (job) => {
  const { url, event, payload } = job.data;

  const signature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(JSON.stringify(payload))
    .digest('hex');

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-Webhook-Event': event,
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok) {
    throw new Error(`Webhook failed: ${response.status}`);
  }
});

Event-Driven Patterns

Fan-Out Pattern

// One event triggers multiple actions
new Worker('events', async (job) => {
  if (job.name === 'user.created') {
    // Fan out to multiple queues
    await Promise.all([
      emailQueue.add('welcome', job.data),
      analyticsQueue.add('track', { event: 'signup', ...job.data }),
      slackQueue.add('notify', { message: `New user: ${job.data.email}` }),
    ]);
  }
});

Saga Pattern

// Multi-step workflow with compensation
async function processOrder(orderId) {
  const steps = [];

  try {
    // Step 1: Reserve inventory
    steps.push(await inventoryQueue.add('reserve', { orderId }));
    await queue.finished(steps[0].id);

    // Step 2: Process payment
    steps.push(await paymentQueue.add('charge', { orderId }));
    await queue.finished(steps[1].id);

    // Step 3: Ship order
    steps.push(await shippingQueue.add('ship', { orderId }));
    await queue.finished(steps[2].id);

  } catch (error) {
    // Compensation: rollback in reverse order
    for (const step of steps.reverse()) {
      await queue.add('compensate', { stepId: step.id });
    }
  }
}
πŸ’‘ Pro Tip

Always use webhook event IDs as job IDs for idempotency. This prevents duplicate processing when providers retry failed deliveries.

Conclusion

flashQ transforms webhook handling from a synchronous bottleneck into a scalable, event-driven architecture. Key benefits:

  • Fast responses: Acknowledge webhooks immediately
  • Reliability: Automatic retries with backoff
  • Idempotency: Prevent duplicate processing
  • Scalability: Handle traffic spikes gracefully

Build Event-Driven Apps

Start handling webhooks reliably with flashQ.

Get Started β†’
ESC