
Bun.js + Stripe Webhooks + Drizzle: платежі без дублювань
Як на Bun.js приймати Stripe webhooks, перевіряти підпис, зберігати event ledger у PostgreSQL і безпечно запускати бізнес-операції один раз.
Bun приймає raw request body, Stripe SDK перевіряє підпис, а PostgreSQL зберігає event ID до запуску бізнес-операції. Повторний event знаходиться в ledger і завершується без повторної видачі товару чи кредитів.
Чому webhook — це не звичайний callback
Stripe може повторити доставку, змінити порядок подій або надіслати подію після тимчасової помилки сервера.
01
Stripe надсилає signed event
Подія містить payload і Stripe-Signature. Не довіряйте JSON до перевірки підпису.
02
Bun перевіряє raw body
Прочитайте request.text(), а не request.json(): зміна whitespace ламає підпис.
03
Ledger перевіряє дубль
Унікальний event.id гарантує, що одна доставка не запускає операцію двічі.
04
Транзакція змінює домен
Запишіть payment status, entitlement або order і processed event в одній транзакції.
Повторна подія зупиняється на idempotency-кроці, а бізнес job запускається тільки після перевірки.
Скріншот секції architectureКрок 1: встановлення і секрети
Встановіть SDK
bun add stripe drizzle-orm postgres
bun add -d drizzle-kitНалаштуйте secrets
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
DATABASE_URL=postgres://app:app@localhost:5432/paymentsWebhook secret для Stripe CLI та production endpoint різні. Не змішуйте їх.
Створіть ledger table
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(),
});Primary key на event ID — остання лінія захисту від дублювання.
Крок 2: перевіряємо підпис на raw body
Найчастіша помилка — викликати request.json() до Stripe constructEvent. Після парсингу байти вже можуть відрізнятися від підписаного 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 перевіряє timestamp tolerance і HMAC-підпис. Не приймайте event, який не пройшов цю перевірку.
Крок 3: idempotent handler із Drizzle
Спочатку вставте event у ledger. Якщо PostgreSQL повернув conflict — це повторна доставка, тому поверніть 200, щоб Stripe не ретраїв її без потреби.
import { eq } from "drizzle-orm";
import { db } from "./db/client";
import { stripeEvents } from "./db/schema";
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 };
}Крок 4: endpoint і retry policy
Поверніть 2xx тільки коли подія успішно перевірена і оброблена або вже відома ledger-у. Для тимчасової помилки бази поверніть 500, щоб Stripe повторив доставку.
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 });
}
}Для важких jobs збережіть event у черзі після commit транзакції. Не робіть довгий network call усередині webhook request без timeout.
Локальна перевірка через Stripe CLI
Прокиньте події локально
stripe listen --forward-to http://localhost:3000/api/stripe/webhookСтворіть тестову подію
stripe trigger checkout.session.completedПеревірте дубль
Надішліть той самий event двічі. У логах має бути одна бізнес-зміна й другий результат { duplicate: true }. Для CI тестуйте підпис із test secret, а не вимикайте verification.
Production checklist
Перевіряйте signature і timestamp
Не використовуйте лише event.type або дані з frontend як доказ оплати.
Зберігайте payload за потреби
Для audit/debug можна зберігати JSON, але обмежте доступ і не логайте секрети.
Обмежуйте права webhook key
Використовуйте окремі secrets і міняйте їх за rotation policy.
Моніторте failed events
Алерти на 5xx, зростання duplicate rate та завислі записи без processedAt.
Часті запитання
Не до перевірки підпису. Спочатку прочитайте raw body через request.text(), перевірте його Stripe SDK, а вже потім працюйте з event data.
Зазвичай 200: event уже оброблено, тому повторювати бізнес-операцію не потрібно. Для тимчасової помилки бази повертайте 500, щоб Stripe повторив доставку.
Висновок
Опишіть задачу — перші 15 хвилин консультації безкоштовні.
Пов'язані статті

Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка
Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.

AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію
Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди
Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.