Pular para o conteúdo
Todos os artigos

Webhooks idempotentes da Stripe e Lemon Squeezy no Next.js

Como verificar assinaturas brutas de webhooks, deduplicar entregas repetidas na fronteira da base de dados e impedir que subscrições sobrepostas revoguem o acesso pago.

a equipa supastack2 min de leitura

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.

Verifica o payload bruto antes de tocar no JSON

Cada fornecedor de pagamentos assina a sequência exata de bytes que envia pela rede. Se um handler de rota analisar await request.json() e voltar a serializar o objeto antes de verificar a assinatura, diferenças na ordem das chaves ou nos espaços em branco invalidarão webhooks legítimos — ou tentarão a equipa a ignorar a verificação em staging.

Lê o corpo bruto uma única vez com await request.text() no teu handler POST do App Router do Next.js, valida a assinatura criptográfica com o segredo exclusivo do servidor e rejeita qualquer falha com HTTP 400 antes de executar uma única query. O guia do módulo de faturação mostra como o supastack isola a verificação de assinaturas atrás de um único ponto de entrada 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 });
}
  • Lê o corpo bruto do pedido com request.text() — nunca voltando a serializar JSON já analisado.
  • Mantém STRIPE_WEBHOOK_SECRET e os segredos de assinatura da Lemon Squeezy apenas na configuração do servidor.
  • Usa comparação em tempo constante (crypto.timingSafeEqual) ao validar assinaturas HMAC manualmente.

Deduplica na fronteira da base de dados, não em memória

A Stripe e a Lemon Squeezy reenviam webhooks sempre que um deploy reinicia, uma função serverless atinge timeout ou um salto de rede perde a resposta. Conjuntos em memória ou chaves de cache efémeras desaparecem entre instâncias e não impedem que duas tentativas simultâneas emitam licenças duplicadas ou enviem dois recibos por email.

Regista o ID de evento do fornecedor ou o estado de liquidação da encomenda dentro da mesma transação Postgres que altera o acesso da subscrição. Um INSERT ... ON CONFLICT DO NOTHING atómico ou uma atualização condicional em status != 'paid' transforma uma repetição concorrente numa operação neutra. Ao desenhar os planos que esses webhooks ativam, o gerador de páginas de preços SaaS ajuda a modelar escalões e regras por lugar antecipadamente.

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;
});

Lida com eventos fora de ordem e subscrições sobrepostas

Não há garantia de que os webhooks cheguem pela ordem em que o utilizador clicou. customer.subscription.updated pode chegar antes de checkout.session.completed, e um cliente que abra acidentalmente dois separadores de checkout pode deter brevemente duas subscrições no mesmo workspace.

Associa cancelamentos e falhas de pagamento ao ID de subscrição específico que originou o evento. Antes de revogar o direito pago de um workspace, confirma se outra subscrição ativa ainda cobre esse workspace — caso contrário, cancelar uma subscrição duplicada remove silenciosamente o acesso pelo qual o cliente continua a pagar.

  • Indexa o estado da subscrição por providerSubscriptionId, e não apenas por workspaceId.
  • Mantém ativo o direito pago de um workspace enquanto pelo menos uma subscrição estiver active ou trialing.
  • Confirma que o montante pago e a moeda correspondem ao SKU esperado antes de cumprir encomendas de pagamento único.

Perguntas frequentes sobre webhooks de faturação SaaS

Estas questões surgem sempre que uma equipa passa a faturação dos testes em sandbox para o tráfego real de produção.

  • Posso desbloquear funcionalidades pagas no redirecionamento de sucesso do checkout? — Não. Os URLs de redirecionamento podem ser guardados nos favoritos, partilhados ou visitados antes da liquidação de métodos de pagamento assíncronos. Usa a página de sucesso para consultar o estado no servidor enquanto o webhook validado concede o acesso.
  • Que código de estado deve devolver um webhook duplicado? — Devolve 200 OK assim que a base de dados confirmar que o evento já foi processado. Devolver 4xx ou 5xx indica ao fornecedor que deve continuar a tentar.
  • Como testo novas tentativas de webhooks localmente? — Escreve testes unitários determinísticos que enviem exatamente o mesmo payload assinado duas vezes e confirma que apenas uma licença, direito ou email transacional é produzido.
  • Como se encaixam as compras in-app móveis no mesmo modelo? — Encaminha as notificações de servidor do RevenueCat pela mesma tabela idempotente de direitos para que as compras web e móveis nunca se substituam mutuamente — consulta a checklist de lançamento para as verificações pré-lançamento.

Perguntas frequentes

14 perguntas

O que é o supastack?

O supastack é uma base modular para construir produtos SaaS com IA. Reúne a infraestrutura comum para que possas concentrar-te nas funcionalidades próprias do teu produto.

Suporta web e mobile?

Sim. O supastack inclui apps Next.js e Expo como pontos de partida para experiências web e mobile.

Que autenticação está incluída?

O Better Auth cobre email/palavra-passe, OTP e login social. Os workspaces pessoais são aprovisionados com a conta para que os registos fiquem delimitados desde o primeiro dia.

Como é suportada a IA?

O supastack inclui IA multi-fornecedor configurável, registo de utilização, orçamentos e suporte encriptado de chaves próprias (BYOK).

Como são geridas as subscrições?

Para produtos construídos sobre o kit, a faturação Stripe, as subscrições e as permissões formam a base do acesso pago. Os segredos de fornecedor ficam no servidor.

Onde vive o código próprio do meu produto?

As funcionalidades próprias do produto ficam num codebase separado, enquanto o produto configura e integra os módulos base do supastack.

Como funciona o preço do supastack?

O kit é uma compra única com atualizações vitalícias: Stack único por 249 $ — à escolha entre os repositórios Next.js (web), Expo (iOS e Android) ou Tauri (macOS e Windows) —, Stack duplo por 398 $ com dois repositórios à escolha, ou Pacote completo por 399 $ com os três repositórios e o monorepo completo, mais 75 $ por lugar de programador adicional. Sem subscrição do kit. O checkout neste site entrega a tua chave de licença por email, com acesso ao repo por convite GitHub.

Como é que os workspaces delimitam os meus dados?

Os registos do utilizador são delimitados por workspaceId. Identidade, permissões, utilização de IA, eventos de faturação e entradas de auditoria ficam dentro do limite do workspace pessoal.

Como funcionam os jobs em segundo plano?

O Trigger.dev é o runtime de jobs explícito. O contrato JobRunner só promete enqueue, status e cancel — a semântica de workflow permanece específica e explícita do Trigger.dev.

Como difere o email da divulgação?

O email transacional cobre fluxos de produto como avisos de conclusão de tarefas. A divulgação fria e as campanhas pertencem ao próprio produto, fora do módulo de email deste kit.

Como funcionam as compras no aplicativo móvel?

O aplicativo móvel inclui compras nativas e restauração via RevenueCat, com acesso verificado pelo servidor. Configure as contas das lojas e conclua os testes em dispositivos antes de publicar. A cobrança web permanece separada.

Onde residem as chaves dos fornecedores de IA?

As credenciais BYOK são encriptadas e ficam no servidor. As chaves são higienizadas nos logs; a utilização e os orçamentos ficam visíveis ao nível do workspace.

Quais métodos de pagamento o checkout aceita?

O checkout hospedado na Stripe aceita cartões e também pode oferecer Alipay e WeChat Pay quando essas carteiras estiverem ativadas na conta Stripe — útil para compradores na China. As autorizações de carteira são confirmadas de forma assíncrona, então a página da licença é atualizada assim que o pagamento é liquidado.

Posso solicitar um reembolso?

Depois de obter acesso ao repositório, o supastack é seu para sempre, portanto não pode ser reembolsado. Mas fique tranquilo: você recebe o código-fonte completo com atualizações vitalícias para criar e lançar produtos ilimitados para você ou seus clientes. Você apenas não pode revender o código nem redistribuir o repositório como starter kit ou modelo. Uma exceção é o cashback de lançamento — lance um produto real feito com supastack, compartilhe-o e alcance 5.000 visualizações orgânicas ou 50 curtidas orgânicas em até 60 dias para receber de volta 50% do valor pago; consulte os termos.

PT