Apps SaaS no começo adoram um flag local IsActive. Parece rápido. Também é o jeito clássico de passar uma sexta reconciliando um cliente que pagou, uma linha que ainda diz plano grátis e um webhook que você já processou duas vezes.

Status é uma máquina de estados, não um checkbox

Assinaturas no Stripe passam por estados de verdade: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid e às vezes paused. Cada um significa algo diferente para o acesso.

incomplete não é "quase ativo". Significa que a primeira fatura não passou. past_due não é cancelado. Retentativas ainda podem funcionar. paused depois de um trial sem método de pagamento não é a mesma coisa que pausar a cobrança enquanto as faturas continuam sendo geradas.

Se o app guarda um booleano e atualiza "quando parece certo", você inventa uma segunda fonte da verdade. Essa segunda fonte diverge.

Espelhe o provedor, não invente a sua

O padrão pragmático é chato e confiável:

  1. Persista o id da assinatura no provedor e a string de status bruta.
  2. Derive o acesso ao produto desse status (e das suas regras de entitlement), não de um flag mantido à mão.
  3. Trate o webhook como o caminho de escrita que atualiza o status. Trate o admin como leitor, não como um livro-razão rival.

Em .NET isso costuma significar mapear o enum de status do Stripe para o seu domínio uma vez, e recusar enums paralelos que quase batem. O "quase" é onde mora o bug.

Webhook sem idempotência cobra duas vezes

Retentativas acontecem. Rede falha. Seu handler vai ver o mesmo event.id mais de uma vez. Sem uma constraint única nesse id (ou uma tabela de claim equivalente), você aplica efeito colateral duas vezes: libera acesso duas vezes, manda email duas vezes ou, pior, muda billing duas vezes.

Verifique assinaturas contra o body cru. Use chaves de idempotência em chamadas API que não sejam GET. Mantenha o handler magro: atualize a projeção local, enfileire trabalho se precisar, responda 200.

Stripe doméstico, Paddle global, um modelo de acesso

Se a cobrança caiu via Stripe em casa ou via Merchant of Record no exterior, o produto ainda precisa de uma resposta só para "essa conta pode usar o recurso hoje?" Deixe detalhes do provedor na borda. Deixe a lógica de entitlement num lugar só, lendo um status normalizado.

Uma fundação code-first que já liga webhooks de billing, projeção de status e uma única checagem de entitlement economiza a tarde que você gastaria depurando por que produção diz pago e o banco diz grátis. Essa tarde sempre custa mais do que a template parecia na página de preço.