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:
- 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.
- Stripe (Processamento): O cliente digita o cartão na tela segura do Stripe. O Stripe processa o saldo e aprova a compra.
- 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.