const { pool } = require('./db');
async function recordEvent(client, { eventId, type, payload }) {
const { rows } = await client.query(
`INSERT INTO webhook_events (event_id, type, payload, status)
VALUES ($1, $2, $3, 'received')
ON CONFLICT (event_id) DO NOTHING
RETURNING id, event_id, status`,
[eventId, type, payload]
);
return rows[0] || null; // null => duplicate delivery
}
async function markProcessed(client, id) {
await client.query(
`UPDATE webhook_events
SET status = 'processed', processed_at = now()
WHERE id = $1`,
[id]
);
}
async function markFailed(client, id, err) {
await client.query(
`UPDATE webhook_events
SET status = 'failed', last_error = $2, attempts = attempts + 1
WHERE id = $1`,
[id, String(err && err.message || err).slice(0, 500)]
);
}
async function withTransaction(fn) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await fn(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
module.exports = { recordEvent, markProcessed, markFailed, withTransaction };
const Stripe = require('stripe');
const { recordEvent, withTransaction } = require('./webhookStore');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
async function verifyAndStore(req, res, next) {
const signature = req.headers['stripe-signature'];
let event;
try {
// req.body is a Buffer here because the route uses express.raw
event = stripe.webhooks.constructEvent(req.body, signature, endpointSecret);
} catch (err) {
return res.status(400).send(`Webhook signature verification failed: ${err.message}`);
}
try {
const stored = await withTransaction((client) =>
recordEvent(client, {
eventId: event.id,
type: event.type,
payload: event,
})
);
if (!stored) {
// Already seen this event id: ack so Stripe stops retrying.
return res.status(200).json({ received: true, duplicate: true });
}
req.stripeEvent = event;
req.storedEvent = stored;
return next();
} catch (err) {
return next(err);
}
}
module.exports = { verifyAndStore, stripe };
const express = require('express');
const { verifyAndStore } = require('./verifyAndStore');
const { markProcessed, markFailed, withTransaction } = require('./webhookStore');
const { fulfillOrder, refundOrder } = require('./orders');
const router = express.Router();
async function handleEvent(client, event) {
switch (event.type) {
case 'checkout.session.completed':
await fulfillOrder(client, event.data.object);
break;
case 'charge.refunded':
await refundOrder(client, event.data.object);
break;
default:
break; // unhandled types are still marked processed
}
}
router.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }),
verifyAndStore,
async (req, res) => {
const { stripeEvent, storedEvent } = req;
try {
await withTransaction(async (client) => {
await handleEvent(client, stripeEvent);
await markProcessed(client, storedEvent.id);
});
return res.status(200).json({ received: true });
} catch (err) {
await withTransaction((client) => markFailed(client, storedEvent.id, err));
// 500 tells Stripe to retry; dedupe protects the reprocess.
return res.status(500).json({ error: 'processing_failed' });
}
}
);
module.exports = router;
Webhook providers guarantee at-least-once delivery, which means the same event can arrive multiple times — on retries, network hiccups, or provider-side redeliveries. Processing a delivery twice can double-charge a customer or send duplicate emails, so the receiver must be idempotent. This snippet shows the common pattern of persisting each delivery to an event store before handling it, keyed by the provider's unique event id, then letting a handler consume events exactly once.
In webhookStore.js, recordEvent performs an INSERT ... ON CONFLICT (event_id) DO NOTHING against a webhook_events table. Postgres treats the event_id as the natural dedupe key: the first delivery inserts a row in the received state and returns it, while any duplicate insert affects zero rows and returns null. That single atomic statement is the whole idempotency guarantee — no read-then-write race window. markProcessed and markFailed transition the row's status so retries and observability are possible.
verifyAndStore.js is the Express middleware that runs first. It needs the raw request body to verify the signature, which is why the route is mounted with express.raw. It calls stripe.webhooks.constructEvent to authenticate the payload — an invalid signature returns 400 immediately, before anything touches the database. Once verified, it records the event and stashes both the parsed event and the freshly-stored row on req. Crucially, if recordEvent returns null the delivery is a duplicate, so the middleware short-circuits with 200 OK; acknowledging duplicates keeps the provider from retrying and never invokes the handler again.
webhookRoutes.js wires it together. The handler only runs for genuinely new events. It dispatches on event.type, does the real work inside a transaction, and calls markProcessed on success. On failure it calls markFailed and returns 500, which signals the provider to retry later — and because the row already exists in a non-processed state, that retry flows back through the same dedupe logic safely.
The trade-off is that business side effects must themselves be transactional or idempotent; the store only prevents reprocessing, not partial work. A cleanup job can later sweep stale received rows for events that crashed mid-flight.
Related snips
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
HMAC signed API requests for webhook and partner integrity
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
Share this code
Here's the card — post it anywhere.