DOMAIN: SaaS com Stripe Subscriptions
Skill gerado a partir do pack templates-claude-code. Arquivo de origem: dominio/01-saas-stripe-billing.md. Use como baseline e adapte ao projeto antes de mudancas grandes.
Conteudo do template
CONTEXT
Billing em SaaS é uma das áreas com mais bugs silenciosos. Webhooks chegam fora de ordem,
trials expiram sem notificação, proration calculation surpreende usuários. A maioria dos times
trata billing como feature simples — e quebra em produção no primeiro upgrade/downgrade.
STACK ASSUMPTIONS
- Payment: Stripe (Billing, Customer Portal, Webhooks)
- Backend: Node.js/Python/Ruby (qualquer — lógica é agnóstica)
- DB: PostgreSQL (com suporte a transações)
- Queue: BullMQ / SQS / Sidekiq para processar webhooks assincronamente
- Cache: Redis para idempotency keys e feature gates
CORE CONCEPTS
- Subscription States: trialing → active → past_due → canceled → unpaid
- Plan Gates: feature availability baseada em
price_id ou metadata do plano
- Proration: crédito/débito calculado por Stripe no upgrade/downgrade mid-cycle
- Billing Anchor: dia do mês em que a cobrança recorre
- Metered Billing: cobrança por uso (API calls, seats, storage) — reportado via usage records
- Payment Intent: objeto que rastreia o ciclo de vida de um pagamento (3DS incluso)
- Customer Portal: UI hospedada pelo Stripe para self-service de billing
- Idempotency Key: header obrigatório em escritas Stripe para evitar duplicatas
ARCHITECTURE RULES
Webhook Handling
- NUNCA processar webhook na request HTTP — enfileirar imediatamente e retornar 200
- Verificar assinatura
stripe-signature header ANTES de qualquer parsing
- Usar
event.id como idempotency key no DB — checar antes de processar
- Handler deve ser idempotente: reprocessar o mesmo evento não deve ter efeito colateral
Subscription Sync
- A fonte de verdade é o Stripe, não seu banco de dados
- Sincronizar estado via webhooks:
customer.subscription.updated, invoice.payment_failed, etc.
- Manter localmente apenas o necessário:
stripe_customer_id, stripe_subscription_id, plan_id, status, current_period_end
- Não confiar em
current_period_end para gates — usar status === 'active'
Plan Gates
- Implementar como middleware/decorator, não if/else espalhado pelo código
- Resolver permissão a partir do plano, não do role do usuário
- Cache de 60s aceitável para feature gates (Redis)
- Sempre retornar
402 Payment Required com body { code: 'UPGRADE_REQUIRED', feature: 'X' }
Trial Periods
- Trial de 14 dias: cobrar cartão na criação do trial (validação sem cobrança)
- Enviar email D-7, D-3, D-1 antes do trial expirar
- Ao expirar trial sem cartão: status →
trialing → canceled (não past_due)
ROUTING TABLE
| Trigger |
Action |
| POST /webhooks/stripe |
Verificar assinatura → enfileirar evento → return 200 |
| invoice.payment_failed |
Atualizar status → past_due → enviar email → iniciar grace period |
| invoice.payment_succeeded |
Atualizar status → active → estender current_period_end |
| customer.subscription.deleted |
Marcar como canceled → revogar acesso → enviar email |
| customer.subscription.trial_will_end |
Disparar sequência de emails (3 dias antes) |
| checkout.session.completed |
Criar/vincular customer → ativar subscription localmente |
| GET /api/billing/portal |
Criar Billing Portal Session → redirect URL |
| POST /api/billing/upgrade |
Criar proration preview → confirmar → atualizar via Stripe API |
| GET /api/billing/invoices |
Listar via Stripe API (não armazenar invoices localmente) |
| POST /api/billing/cancel |
Cancelar com cancel_at_period_end: true (nunca imediato por padrão) |
| Metered: POST /api/usage |
Criar Usage Record no Stripe com timestamp do evento |
| GET /api/plan-features |
Resolver feature gates do plano atual (cache Redis) |
CRITICAL RULES
- NUNCA confiar no frontend para informar o plano — sempre buscar do Stripe/DB no backend
- NUNCA deletar Customer no Stripe — apenas cancelar subscription (histórico fiscal)
- SEMPRE verificar
stripe-signature — ignorar eventos sem verificação é CVE
- SEMPRE usar
idempotency_key em toda chamada de escrita à API do Stripe
- SEMPRE processar webhooks assincronamente — timeout de 10s pode fazer Stripe reentregar
- Ao fazer upgrade: calcular e mostrar proration ANTES de confirmar (
preview_invoice)
past_due ≠ canceled — usuário ainda tem acesso por grace period (configurável no Stripe)
- Metered billing precisa de Usage Record com
timestamp exato — não hora atual do servidor
- Nunca armazenar número de cartão — apenas
pm_ token do Stripe
- Testar com Stripe CLI:
stripe listen --forward-to localhost:3000/webhooks/stripe
COMMON PITFALLS
❌ Processar webhook na request
// ERRADO
app.post('/webhook', async (req, res) => {
await processSubscriptionChange(req.body); // pode demorar, Stripe vai reenviar
res.sendStatus(200);
});
// CORRETO
app.post('/webhook', async (req, res) => {
await queue.add('stripe-event', req.body); // enfileira imediatamente
res.sendStatus(200); // responde em < 1s
});
❌ Usar current_period_end como gate
// ERRADO — usuário pode estar past_due mas dentro do período
if (subscription.current_period_end > Date.now()) { allowAccess(); }
// CORRETO
if (subscription.status === 'active' || subscription.status === 'trialing') { allowAccess(); }
❌ Cancelar imediatamente sem confirmação
// ERRADO — cancela agora, usuário perde acesso
await stripe.subscriptions.cancel(subId);
// CORRETO — cancela no fim do período pago
await stripe.subscriptions.update(subId, { cancel_at_period_end: true });
QUALITY GATES
FORBIDDEN
- Armazenar PAN (número de cartão) em qualquer storage
- Processar lógica de negócio dentro do handler de webhook HTTP
- Usar
event.data.object sem verificar event.type primeiro
- Chamar Stripe API dentro de transaction de banco de dados (latência + deadlock risk)
- Cancelar subscription imediatamente sem fluxo de confirmação com o usuário
- Confiar em campos do frontend como
plan ou priceId sem validar no backend
1---2name: tpl-dominio-saas-stripe-billing3description: Template do pack (dominio/01-saas-stripe-billing.md). Orienta o agente em regras de negocio e requisitos de produto alinhado a esse contexto.4---56# DOMAIN: SaaS com Stripe Subscriptions78Skill gerado a partir do pack `templates-claude-code`. Arquivo de origem: `dominio/01-saas-stripe-billing.md`. Use como baseline e adapte ao projeto antes de mudancas grandes.910## Conteudo do template1112## CONTEXT13Billing em SaaS é uma das áreas com mais bugs silenciosos. Webhooks chegam fora de ordem,14trials expiram sem notificação, proration calculation surpreende usuários. A maioria dos times15trata billing como feature simples — e quebra em produção no primeiro upgrade/downgrade.1617## STACK ASSUMPTIONS18- Payment: Stripe (Billing, Customer Portal, Webhooks)19- Backend: Node.js/Python/Ruby (qualquer — lógica é agnóstica)20- DB: PostgreSQL (com suporte a transações)21- Queue: BullMQ / SQS / Sidekiq para processar webhooks assincronamente22- Cache: Redis para idempotency keys e feature gates2324## CORE CONCEPTS25- **Subscription States**: trialing → active → past_due → canceled → unpaid26- **Plan Gates**: feature availability baseada em `price_id` ou metadata do plano27- **Proration**: crédito/débito calculado por Stripe no upgrade/downgrade mid-cycle28- **Billing Anchor**: dia do mês em que a cobrança recorre29- **Metered Billing**: cobrança por uso (API calls, seats, storage) — reportado via usage records30- **Payment Intent**: objeto que rastreia o ciclo de vida de um pagamento (3DS incluso)31- **Customer Portal**: UI hospedada pelo Stripe para self-service de billing32- **Idempotency Key**: header obrigatório em escritas Stripe para evitar duplicatas3334## ARCHITECTURE RULES3536### Webhook Handling37- NUNCA processar webhook na request HTTP — enfileirar imediatamente e retornar 20038- Verificar assinatura `stripe-signature` header ANTES de qualquer parsing39- Usar `event.id` como idempotency key no DB — checar antes de processar40- Handler deve ser **idempotente**: reprocessar o mesmo evento não deve ter efeito colateral4142### Subscription Sync43- A fonte de verdade é o Stripe, não seu banco de dados44- Sincronizar estado via webhooks: `customer.subscription.updated`, `invoice.payment_failed`, etc.45- Manter localmente apenas o necessário: `stripe_customer_id`, `stripe_subscription_id`, `plan_id`, `status`, `current_period_end`46- Não confiar em `current_period_end` para gates — usar `status === 'active'`4748### Plan Gates49- Implementar como middleware/decorator, não if/else espalhado pelo código50- Resolver permissão a partir do plano, não do role do usuário51- Cache de 60s aceitável para feature gates (Redis)52- Sempre retornar `402 Payment Required` com body `{ code: 'UPGRADE_REQUIRED', feature: 'X' }`5354### Trial Periods55- Trial de 14 dias: cobrar cartão na criação do trial (validação sem cobrança)56- Enviar email D-7, D-3, D-1 antes do trial expirar57- Ao expirar trial sem cartão: status → `trialing` → `canceled` (não `past_due`)5859## ROUTING TABLE60| Trigger | Action |61|---------|--------|62| POST /webhooks/stripe | Verificar assinatura → enfileirar evento → return 200 |63| invoice.payment_failed | Atualizar status → `past_due` → enviar email → iniciar grace period |64| invoice.payment_succeeded | Atualizar status → `active` → estender `current_period_end` |65| customer.subscription.deleted | Marcar como `canceled` → revogar acesso → enviar email |66| customer.subscription.trial_will_end | Disparar sequência de emails (3 dias antes) |67| checkout.session.completed | Criar/vincular customer → ativar subscription localmente |68| GET /api/billing/portal | Criar Billing Portal Session → redirect URL |69| POST /api/billing/upgrade | Criar proration preview → confirmar → atualizar via Stripe API |70| GET /api/billing/invoices | Listar via Stripe API (não armazenar invoices localmente) |71| POST /api/billing/cancel | Cancelar com `cancel_at_period_end: true` (nunca imediato por padrão) |72| Metered: POST /api/usage | Criar Usage Record no Stripe com timestamp do evento |73| GET /api/plan-features | Resolver feature gates do plano atual (cache Redis) |7475## CRITICAL RULES761. **NUNCA** confiar no frontend para informar o plano — sempre buscar do Stripe/DB no backend772. **NUNCA** deletar Customer no Stripe — apenas cancelar subscription (histórico fiscal)783. **SEMPRE** verificar `stripe-signature` — ignorar eventos sem verificação é CVE794. **SEMPRE** usar `idempotency_key` em toda chamada de escrita à API do Stripe805. **SEMPRE** processar webhooks assincronamente — timeout de 10s pode fazer Stripe reentregar816. Ao fazer upgrade: calcular e mostrar proration ANTES de confirmar (`preview_invoice`)827. `past_due` ≠ `canceled` — usuário ainda tem acesso por grace period (configurável no Stripe)838. Metered billing precisa de Usage Record com `timestamp` exato — não hora atual do servidor849. Nunca armazenar número de cartão — apenas `pm_` token do Stripe8510. Testar com Stripe CLI: `stripe listen --forward-to localhost:3000/webhooks/stripe`8687## COMMON PITFALLS8889### ❌ Processar webhook na request90```javascript91// ERRADO92app.post('/webhook', async (req, res) => {93 await processSubscriptionChange(req.body); // pode demorar, Stripe vai reenviar94 res.sendStatus(200);95});9697// CORRETO98app.post('/webhook', async (req, res) => {99 await queue.add('stripe-event', req.body); // enfileira imediatamente100 res.sendStatus(200); // responde em < 1s101});102```103104### ❌ Usar current_period_end como gate105```javascript106// ERRADO — usuário pode estar past_due mas dentro do período107if (subscription.current_period_end > Date.now()) { allowAccess(); }108109// CORRETO110if (subscription.status === 'active' || subscription.status === 'trialing') { allowAccess(); }111```112113### ❌ Cancelar imediatamente sem confirmação114```javascript115// ERRADO — cancela agora, usuário perde acesso116await stripe.subscriptions.cancel(subId);117118// CORRETO — cancela no fim do período pago119await stripe.subscriptions.update(subId, { cancel_at_period_end: true });120```121122## QUALITY GATES123- [ ] Webhook endpoint verifica `stripe-signature` antes de qualquer parsing124- [ ] Todos os eventos têm idempotency check no banco antes de processar125- [ ] Feature gates implementados como middleware centralizado126- [ ] Proration preview exibida antes de confirmar upgrade/downgrade127- [ ] Trial expiry emails agendados em D-7, D-3, D-1128- [ ] Cancelamento usa `cancel_at_period_end: true` por padrão129- [ ] Metered usage reportado com timestamp do evento, não do servidor130- [ ] Stripe CLI utilizado em desenvolvimento para simular webhooks131- [ ] Test mode e live mode com variáveis de ambiente separadas132- [ ] Logs estruturados para toda interação com Stripe API (para debugging de billing)133134## FORBIDDEN135- Armazenar PAN (número de cartão) em qualquer storage136- Processar lógica de negócio dentro do handler de webhook HTTP137- Usar `event.data.object` sem verificar `event.type` primeiro138- Chamar Stripe API dentro de transaction de banco de dados (latência + deadlock risk)139- Cancelar subscription imediatamente sem fluxo de confirmação com o usuário140- Confiar em campos do frontend como `plan` ou `priceId` sem validar no backend