Webhook Processing Architecture at Pre-Seed Stage
At pre-seed, your webhook needs are simpler than the full architecture a solo or seed-stage team builds — but the non-negotiables are the same. Signature verification and basic idempotency are required regardless of stage. Everything else is a decision about what to defer until you have the volume that justifies the complexity.
This guide covers the pre-seed webhook architecture: what you must build now, what you can build later, and the two failure modes that will cause real production incidents if you skip them.
📋 Pre-Seed Webhook Requirements
| Requirement | Pre-Seed Priority | Reason |
|---|---|---|
| Signature verification | Required immediately | Security — anyone can send fake events without it |
| Basic idempotency | Required immediately | At-least-once delivery means you will receive duplicates |
| 200 response on receipt | Required immediately | Providers retry if you do not acknowledge quickly |
| Event type router | Build early | Prevents unmanageable switch statements as events grow |
| Async job queue | Defer until needed | Only required when processing exceeds 5 seconds |
| Retry monitoring | Defer until needed | Add when webhook failures cause user-visible problems |
🔐 Signature Verification (Do This Today)
Every webhook provider signs their payloads. Always verify the signature before processing. This is a 10-line change and the highest-priority security task in your webhook stack.
For Stripe (the most common pre-seed webhook source):
import Stripe from 'stripe';
export async function POST(req: Request) {
const body = await req.text(); // Must use raw body
const sig = req.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new Response('Invalid signature', { status: 400 });
}
await handleEvent(event);
return new Response('OK', { status: 200 });
}The critical detail: use the raw request body string, not the parsed JSON. Parsing first will break signature verification because JSON serialization can reorder keys.
🔁 Basic Idempotency
Webhook providers guarantee at-least-once delivery. You will receive the same event more than once — especially if your endpoint returns an error or times out. Without idempotency, a duplicate payment.succeeded event can provision two accounts, send two confirmation emails, or apply a credit twice.
The pre-seed idempotency solution is a single database table:
CREATE TABLE processed_webhooks (
event_id TEXT PRIMARY KEY,
processed_at TIMESTAMPTZ DEFAULT NOW()
);
async function handleEvent(event) {
// Try to insert — fails on duplicate
try {
await db.query(
'INSERT INTO processed_webhooks (event_id) VALUES ($1)',
[event.id]
);
} catch (err) {
if (err.code === '23505') return; // Already processed
throw err;
}
// Process the event
await processEvent(event);
}This is atomic (the database uniqueness constraint acts as the lock), requires no additional infrastructure, and handles concurrent requests correctly.
⚡ Synchronous vs Async Processing at Pre-Seed
At pre-seed, process webhooks synchronously unless you have a specific operation that takes more than 5 seconds. Sending a welcome email, updating an account record, and writing an audit log entry are all fast enough to do synchronously within the webhook request cycle.
Add a job queue (Inngest, BullMQ) when you have:
- → Multi-step provisioning flows that take 10+ seconds
- → Downstream API calls that can fail and need independent retry
- → High-volume events where synchronous processing creates latency spikes
At pre-seed, none of these conditions are usually met. Synchronous processing is simpler, easier to debug, and correct for small volumes. Optimize when you have a problem, not before.
What to Do Next
If your webhook endpoint has no signature verification: add it before your next production deploy. It is the highest-priority security fix in your stack and takes 10 minutes. If you have verification but no idempotency: add the processed_webhooks table and the duplicate check before your next incident. If you have both: verify that you return 200 for unrecognized event types (return 400 only for signature failures, not for events you do not handle — returning 400 causes some providers to retry indefinitely).