Перейти к содержимому
Все статьи

Идемпотентные вебхуки Stripe и Lemon Squeezy в Next.js

Как проверять подписи необработанных вебхуков, дедуплицировать повторные доставки на границе базы данных и не позволять пересекающимся подпискам отзывать платный доступ.

команда supastack2 мин чтения

How do I handle Stripe and Lemon Squeezy webhooks idempotently in Next.js?

It depends on your billing providers, but the core architecture is identical: verify the raw payload signature before parsing JSON, normalize provider events into a shared internal contract, and enforce idempotency at the database boundary using the provider's event or order ID. Never grant product entitlements from a client-side checkout redirect; let the verified webhook transactionally update subscription state and entitlements once. supastack prewires this pipeline for Stripe, Lemon Squeezy, and mobile RevenueCat, though single-plan apps can start with a simpler handler.

Проверяйте необработанный payload до разбора JSON

Каждый платёжный провайдер подписывает точную последовательность байтов, отправляемую по сети. Если обработчик маршрута вызывает await request.json() и заново сериализует объект перед проверкой подписи, различия в порядке ключей или пробелах сделают легитимные вебхуки недействительными — или подтолкнут разработчиков отключить проверку на стейджинге.

Прочитайте необработанное тело один раз через await request.text() в обработчике POST Next.js App Router, проверьте криптографическую подпись с помощью серверного секрета вебхука и отклоняйте любые несовпадения с кодом HTTP 400 до выполнения первого запроса к базе. Руководство по модулю биллинга показывает, как supastack изолирует проверку подписей за единой точкой входа BFF.

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  const event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    env.STRIPE_WEBHOOK_SECRET,
  );
  await handleNormalizedBillingEvent(event);
  return Response.json({ received: true });
}
  • Читайте необработанное тело запроса через request.text() — никогда не сериализуйте разобранный JSON повторно.
  • Храните STRIPE_WEBHOOK_SECRET и секреты подписи Lemon Squeezy исключительно в серверной конфигурации.
  • Используйте сравнение за постоянное время (crypto.timingSafeEqual) при ручной проверке подписей HMAC.

Дедуплицируйте на границе базы данных, а не в памяти

Stripe и Lemon Squeezy повторяют отправку вебхуков при перезапуске деплоя, таймауте бессерверной функции или потере ответа в сети. Локальные для процесса коллекции или короткоживущие ключи кэша теряются между инстансами и не помешают двум одновременным повторам выдать дубликат лицензии или отправить два письма с квитанцией.

Записывайте ID события провайдера или статус оплаты заказа в той же транзакции Postgres, которая изменяет доступ по подписке. Атомарный INSERT ... ON CONFLICT DO NOTHING или условное обновление по status != 'paid' превращает параллельный повтор в безопасную пустую операцию. При проектировании тарифов, которые активируют эти вебхуки, генератор страницы тарифов SaaS помогает заранее смоделировать уровни и правила расчёта мест.

const settled = await db.transaction(async (tx) => {
  const [inserted] = await tx
    .insert(billingWebhookEvents)
    .values({ providerEventId: event.id, provider: "stripe" })
    .onConflictDoNothing({ target: billingWebhookEvents.providerEventId })
    .returning({ id: billingWebhookEvents.id });

  if (!inserted) return false; // Already processed
  await applyWorkspaceEntitlement(tx, event);
  return true;
});

Обрабатывайте события вне очереди и пересекающиеся подписки

Вебхуки не обязаны приходить в том порядке, в котором нажимал пользователь. customer.subscription.updated может прийти раньше checkout.session.completed, а клиент, случайно открывший две вкладки оплаты, может на короткое время получить две подписки в одном воркспейсе.

Привязывайте отмены и ошибки оплаты к конкретному ID подписки, вызвавшему событие. Прежде чем отзывать платный доступ воркспейса, проверьте, не покрывает ли этот воркспейс другая активная подписка — иначе отмена дублирующей подписки тихо лишит клиента доступа, за который он продолжает платить.

  • Храните состояние подписки по ключу providerSubscriptionId, а не только по workspaceId.
  • Сохраняйте платный доступ воркспейса активным, пока хотя бы одна подписка находится в статусе active или trialing.
  • Проверяйте совпадение оплаченной суммы и валюты с ожидаемым SKU перед выполнением разовых заказов.

Частые вопросы о платёжных вебхуках в SaaS

Эти вопросы возникают всякий раз, когда команда переводит биллинг из песочницы на реальный продакшен-трафик.

  • Можно ли открывать платные функции сразу при редиректе на страницу успешной оплаты? — Нет. Ссылку редиректа можно сохранить в закладки, переслать или открыть до завершения асинхронного платежа. Используйте страницу успеха для опроса состояния сервера, пока проверенный вебхук выдаёт доступ.
  • Какой код ответа должен возвращать дублирующийся вебхук? — Возвращайте 200 OK, как только база данных подтвердит, что событие уже обработано. Ответ 4xx или 5xx заставит провайдера продолжать повторные попытки.
  • Как локально тестировать повторную доставку вебхуков? — Напишите детерминированные модульные тесты, которые дважды отправляют один и тот же подписанный payload, и проверьте, что создаётся только одна лицензия, одно право доступа или одно транзакционное письмо.
  • Как мобильные встроенные покупки вписываются в ту же модель? — Направляйте серверные уведомления RevenueCat в ту же идемпотентную таблицу прав доступа, чтобы веб- и мобильные покупки никогда не перезаписывали друг друга — список проверок перед релизом есть в чек-листе запуска.

Часто задаваемые вопросы

14 вопросов

Что такое supastack?

supastack — модульная основа для создания SaaS-продуктов с AI. Она объединяет общую инфраструктуру, чтобы вы могли сосредоточиться на продуктовых функциях.

Поддерживает ли он web и mobile?

Да. supastack включает приложения Next.js и Expo как отправные точки для web- и мобильных интерфейсов.

Какая аутентификация включена?

Better Auth покрывает e-mail/пароль, OTP и социальный вход. Личные воркспейсы провижинируются вместе с аккаунтом, чтобы продуктовые записи были ограничены с первого дня.

Как он поддерживает AI?

supastack включает настраиваемый мультипровайдерный AI, учёт использования, бюджеты и зашифрованную поддержку bring-your-own-key.

Как обрабатываются подписки?

Для продуктов на ките Stripe-биллинг, подписки и права доступа образуют основу платного доступа. Секреты провайдеров остаются только на сервере.

Где живёт мой продуктовый код?

Продуктовые функции остаются в отдельном коде, а продукт настраивает и интегрирует модули основы supastack.

Как устроены цены supastack?

Кит — разовая покупка с пожизненными обновлениями: Single Stack за $249 — на выбор репозиторий Next.js (веб), Expo (iOS и Android) или Tauri (macOS и Windows) —, Dual Stack за $398 с любыми двумя репозиториями или Full Bundle за $399 со всеми тремя репозиториями и полным монорепо, плюс $75 за каждое дополнительное место разработчика. Без подписки на кит. Оплата на этом сайте доставляет лицензионный ключ по e-mail, доступ к репозиторию — через приглашение GitHub.

Как воркспейсы ограничивают мои данные?

Записи пользователей ограничены по workspaceId. Идентичность, права доступа, AI-использование, события биллинга и записи аудита остаются внутри границы личного воркспейса.

Как работают фоновые задачи?

Trigger.dev — явный рантайм задач. Контракт JobRunner обещает только enqueue, status и cancel — семантика workflow остаётся специфичной для Trigger.dev и явной.

Чем почта отличается от рассылок?

Транзакционная почта покрывает продуктовые сценарии вроде уведомлений о завершении. Холодные рассылки и кампании — забота продукта, вне почтового модуля этого кита.

Как работают покупки в мобильном приложении?

Мобильное приложение поддерживает нативные покупки и восстановление через RevenueCat, а сервер проверяет доступ. Настройте аккаунты магазинов и завершите тестирование на устройствах перед публикацией. Веб-оплата остается отдельной.

Где хранятся ключи AI-провайдеров?

Bring-your-own-key креденшелы зашифрованы и остаются только на сервере. Ключи удаляются из логов; использование и бюджеты видны на уровне воркспейса.

Какие способы оплаты принимает оформление заказа?

Оформление заказа на Stripe принимает карты, а после включения кошельков в аккаунте Stripe может также предлагать Alipay и WeChat Pay — удобно для покупателей из Китая. Авторизации кошельков подтверждаются асинхронно, поэтому страница лицензии обновляется, как только платёж завершается.

Могу ли я получить возврат средств?

После получения доступа к репозиторию supastack остается вашим навсегда, поэтому возврат средств невозможен. Но будьте уверены: вы получаете полный исходный код с пожизненными обновлениями для создания и запуска неограниченного числа продуктов для себя или клиентов. Вы лишь не можете перепродавать код или распространять репозиторий как стартовый набор или шаблон. Единственное исключение — кешбэк за запуск: запустите рабочий продукт на supastack, расскажите о нём и наберите 5 000 органических просмотров или 50 органических лайков в течение 60 дней, чтобы получить обратно 50% уплаченной суммы; см. условия.

RU