Capítulo 32 de 35

Integração Segura de Pagamentos: Stripe Webhooks e Supabase Edge Functions

Índice do Manual

Se existe uma área onde você não pode errar o código, é no faturamento. Um erro na validação de um pagamento permite que usuários burlem o sistema e acessem o seu SaaS gratuitamente. Um erro de lentidão faz com que o cliente pague, mas a tela dele não atualize o status da assinatura.

A esmagadora maioria dos tutoriais na internet ensina a integrar o Stripe de forma irresponsável: eles mandam você criar uma Rota de API no seu servidor principal (Next.js ou Node.js) para escutar os "Avisos de Pagamento" (Webhooks) do Stripe. Isso é um erro arquitetural amador.

Como Especialista em Sistemas e Integrações, a regra para o processamento financeiro é clara: Webhooks não devem disputar processamento com o tráfego do usuário no servidor web.

Eles devem ser isolados em Edge Functions independentes. Neste manual, documento a estrutura exata de como orquestrar a triangulação Next.js (Visual) -> Stripe (Motor) -> Supabase (Cérebro).


1. O Fluxo Arquitetural (O Caminho do Dinheiro)

Para garantir segurança militar e alta disponibilidade, a integração funciona em três etapas isoladas:

  1. Next.js (Frontend): O cliente clica em "Assinar". O Next.js chama a API do Stripe para gerar um Link de Pagamento (Checkout Session) e redireciona o usuário. A responsabilidade do Next.js acaba aqui.
  2. Stripe (Processamento): O cliente digita o cartão na tela segura do Stripe. O Stripe processa o saldo e aprova a compra.
  3. Supabase Edge Function (O Recebedor Invisível): No milissegundo em que o cartão é aprovado, o Stripe dispara um POST (Webhook) invisível pelas costas da internet. Esse POST não vai para o Next.js; ele bate diretamente em uma Edge Function (Deno) hospedada no Supabase.

2. O Código da Edge Function (Deno)

A Edge Function do Supabase é um ambiente isolado que roda código Deno em servidores espalhados pelo mundo. A única função deste script é receber o aviso do Stripe, confirmar se o aviso não é de um hacker falsificando requisições, e atualizar o plano do cliente no Postgres.

Abaixo está o código-fonte padrão de segurança máxima.

// supabase/functions/stripe-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import Stripe from 'https://esm.sh/stripe@11.1.0?target=deno'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1'

// Inicializando o Stripe com a chave secreta
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') as string, {
  apiVersion: '2022-11-15',
  httpClient: Stripe.createFetchHttpClient(),
})

serve(async (req) => {
  // 1. Lemos a assinatura de criptografia enviada no cabeçalho do Stripe
  const signature = req.headers.get('Stripe-Signature')
  const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')
  
  if (!signature || !webhookSecret) {
    return new Response('Sem Assinatura', { status: 400 })
  }

  // 2. Lemos o corpo cru da requisição para validação
  const body = await req.text()
  let event;

  try {
    // 3. O PASSO CRÍTICO: Validando criptograficamente se o POST 
    // realmente veio dos servidores oficiais do Stripe.
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
  } catch (err) {
    console.error(`Falha na validação do Webhook: ${err.message}`)
    return new Response(`Erro: ${err.message}`, { status: 400 })
  }

  // 4. Se a assinatura for válida, abrimos o cofre do Supabase (Service Role)
  // Atenção: Use o SERVICE_ROLE apenas em Edge Functions seguras para ignorar o RLS
  const supabaseAdmin = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  )

  // 5. Roteamento Lógico do Pagamento
  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object;
      
      // O Stripe nos avisa que pagou. Atualizamos a tabela 'subscriptions' no Postgres
      const { error } = await supabaseAdmin
        .from('subscriptions')
        .update({ status: 'active', stripe_customer_id: session.customer })
        .eq('user_id', session.client_reference_id)
        
      if (error) throw error;
      break;
      
    case 'customer.subscription.deleted':
      // Se o cliente cancelar, rebaixamos o plano dele na hora
      const deletedSession = event.data.object;
      await supabaseAdmin
        .from('subscriptions')
        .update({ status: 'canceled' })
        .eq('stripe_customer_id', deletedSession.customer)
      break;
  }

  // 6. Retornamos 200 OK rapidamente para o Stripe parar de tentar enviar o aviso
  return new Response(JSON.stringify({ received: true }), { status: 200 })
})

O Triunfo da Estabilidade

Quando você desenha a integração financeira dessa forma, o seu SaaS ganha resiliência absoluta.

Se o seu servidor web principal (Next.js) estiver sofrendo um ataque e sair do ar por 10 minutos, o webhook do Stripe continuará sendo recebido e processado normalmente pela malha distribuída do Deno no Supabase. Nenhum pagamento é perdido, e o plano do cliente é ativado de forma garantida.