
Bun.js + Stripe Webhooks + Drizzle: payments without duplicates
Handle Stripe webhooks on Bun.js with signature verification, a PostgreSQL event ledger, idempotency, and transaction-safe business operations.
Bun reads the raw request body, the Stripe SDK verifies its signature, and PostgreSQL stores the event ID before business work starts. A duplicate event is found in the ledger and becomes a no-op.
Why a webhook is not a normal callback
Stripe may retry delivery, change event order, or deliver after a temporary server failure.
01
Stripe sends a signed event
The payload includes Stripe-Signature. Do not trust JSON before verification.
02
Bun verifies the raw body
Read request.text(), not request.json(): parsing can change the signed bytes.
03
The ledger catches duplicates
A unique event.id prevents one delivery from running business work twice.
04
A transaction changes the domain
Update payment status or entitlement and mark the event processed atomically.
Duplicate delivery stops at idempotency; the business job runs only after verification.
Section architecture screenshotStep 1: install and store secrets
Install the SDK
bun add stripe drizzle-orm postgres
bun add -d drizzle-kitConfigure secrets
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
DATABASE_URL=postgres://app:app@localhost:5432/paymentsThe Stripe CLI secret and production endpoint secret are different. Keep them separate.
Create the event ledger
export const stripeEvents = pgTable("stripe_events", {
id: text("id").primaryKey(),
type: text("type").notNull(),
processedAt: timestamp("processed_at", { withTimezone: true }),
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow().notNull(),
});The primary key is the final duplicate-delivery guard.
Step 2: verify the raw body
The common mistake is calling request.json() before constructEvent. The bytes may no longer match the signed payload.
import Stripe from "stripe";
const stripe = new Stripe(Bun.env.STRIPE_SECRET_KEY!);
export async function verifyStripeRequest(request: Request) {
const signature = request.headers.get("stripe-signature");
if (!signature) throw new Response("Missing signature", { status: 400 });
const rawBody = await request.text();
return stripe.webhooks.constructEvent(rawBody, signature, Bun.env.STRIPE_WEBHOOK_SECRET!);
}constructEvent verifies the HMAC signature and timestamp tolerance. Reject anything that fails it.
Step 3: an idempotent Drizzle handler
Insert the event first. A conflict means Stripe delivered it again, so return 200 without repeating fulfillment.
export async function processEvent(event: Stripe.Event) {
const inserted = await db.insert(stripeEvents).values({ id: event.id, type: event.type }).onConflictDoNothing().returning();
if (inserted.length === 0) return { duplicate: true };
await db.transaction(async (tx) => {
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
// tx.update(orders).set({ status: "paid" }).where(eq(orders.id, session.client_reference_id!));
}
await tx.update(stripeEvents).set({ processedAt: new Date() }).where(eq(stripeEvents.id, event.id));
});
return { duplicate: false };
}Step 4: endpoint and retry policy
Return 2xx only after the event is verified and processed, or already present in the ledger. Return 500 for a temporary database failure so Stripe retries.
export async function POST(request: Request) {
try {
const event = await verifyStripeRequest(request);
await processEvent(event);
return new Response("ok", { status: 200 });
} catch (error) {
console.error("stripe webhook failed", error);
return new Response("retry", { status: 500 });
}
}For slow jobs, enqueue after the database commit. Do not make an unbounded network call inside the webhook request.
Test locally with Stripe CLI
Forward events
stripe listen --forward-to http://localhost:3000/api/stripe/webhookTrigger an event
stripe trigger checkout.session.completedVerify duplicates
Send the same event twice. You should see one business change and a second { duplicate: true } result. Never disable signature verification in CI.
Production checklist
Verify signature and timestamp
Frontend data or event type alone is not proof of payment.
Protect payload data
Store JSON only when needed for audit; restrict access and redact secrets from logs.
Rotate webhook secrets
Use separate secrets per environment and follow a rotation policy.
Monitor failed events
Alert on 5xx responses, rising duplicate rates, and unprocessed ledger rows.
FAQ
Not before signature verification. Read request.text(), verify it with the Stripe SDK, and only then use event data.
Usually 200: it was already processed. Return 500 for a temporary database failure so Stripe retries.
Conclusion
Describe the task — first 15 minutes of consultation are free.
Related Articles

AI Assistant Development Cost in 2026: RAG Chatbots, CRM Integrations, Guardrails, and Support
A practical buyer guide to AI assistant development cost in 2026: prototypes, RAG chatbots, knowledge-base assistants, CRM and website integrations, guardrails, evaluations, monitoring, and support.

AI for landing page development: where it speeds up launches and where it hurts conversion
A practical research piece on using AI for landing page development: v0, Webflow AI, Builder.io, Framer-like builders, UX generation, copy, SEO, personalization, A/B testing, template risk, accessibility, security and technical debt.

AI SEO / GEO in 2026: Your Next Customers Aren’t Humans — They’re Agents
Search is shifting from clicks to answers. Bots and AI agents crawl, cite, recommend, and increasingly buy. Learn what AI SEO / GEO means, why classic SEO is no longer enough, and how PAS7 Studio helps brands win visibility in the agentic web.

The most powerful Apple chip yet? M5 Pro and M5 Max are breaking records
A data-backed March 2026 analysis of Apple M5 Pro and M5 Max. We break down why these chips can credibly be called Apple's most powerful pro laptop silicon, how they compare with M4 Pro, M4 Max, M1 Pro, M1 Max, and how they stack up against Intel and AMD laptop rivals.
Professional development for your business
We create modern web solutions and bots for businesses. Learn how we can help you achieve your goals.