nexus-core-v3 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/agents/README.md +133 -0
- package/agents/_protocol.md +107 -0
- package/agents/analyst.md +138 -0
- package/agents/architect.md +146 -0
- package/agents/data-engineer.md +170 -0
- package/agents/dev.md +134 -0
- package/agents/devops.md +141 -0
- package/agents/nexus-master.md +147 -0
- package/agents/pm.md +133 -0
- package/agents/po.md +138 -0
- package/agents/qa.md +192 -0
- package/agents/sm.md +122 -0
- package/agents/squad-creator.md +121 -0
- package/agents/ux-design-expert.md +165 -0
- package/artifact-manifest.json +903 -0
- package/bin/nexus.mjs +37 -0
- package/checklists/README.md +49 -0
- package/checklists/architect-checklist.md +47 -0
- package/checklists/change-checklist.md +61 -0
- package/checklists/db-predeploy-checklist.md +57 -0
- package/checklists/design-quality-checklist.md +57 -0
- package/checklists/discovery-checklist.md +36 -0
- package/checklists/foundation-checklist.md +39 -0
- package/checklists/launch-checklist.md +39 -0
- package/checklists/pm-checklist.md +48 -0
- package/checklists/po-master-checklist.md +64 -0
- package/checklists/reality-check-checklist.md +49 -0
- package/checklists/story-dod-checklist.md +52 -0
- package/checklists/story-draft-checklist.md +36 -0
- package/dist/bin/dashboard.html +279 -0
- package/dist/bin/nexus.mjs +20008 -0
- package/dist/constitution.yaml +76 -0
- package/knowledge/README.md +57 -0
- package/knowledge/architecture/architectural-styles-map.md +182 -0
- package/knowledge/architecture/design-patterns-gof.md +192 -0
- package/knowledge/architecture/distributed-patterns-cheatsheet.md +201 -0
- package/knowledge/architecture/saas-subscription-blueprint.md +355 -0
- package/knowledge/architecture/system-design-tradeoffs.md +231 -0
- package/knowledge/architecture/t3-fullstack-typesafe-stack.md +273 -0
- package/knowledge/copy/landing-copy-that-converts.md +168 -0
- package/knowledge/data/postgres-indexing-and-tuning.md +263 -0
- package/knowledge/data/schema-modeling-decisions.md +273 -0
- package/knowledge/data/supabase-rls-patterns.md +316 -0
- package/knowledge/data/zero-downtime-migrations.md +308 -0
- package/knowledge/devops/cicd-pipeline-best-practices.md +318 -0
- package/knowledge/devops/production-dockerfile.md +283 -0
- package/knowledge/devops/twelve-factor-app.md +398 -0
- package/knowledge/engineering/clean-code-principles.md +429 -0
- package/knowledge/engineering/effective-code-review.md +204 -0
- package/knowledge/engineering/testing-strategy-beyond-unit.md +307 -0
- package/knowledge/governance/risk-matrix.md +56 -0
- package/knowledge/integration/mcp-server-selection-matrix.md +235 -0
- package/knowledge/marketing/copy-que-converte.md +43 -0
- package/knowledge/marketing/funil-e-jornada.md +36 -0
- package/knowledge/negocios/proposta-vencedora.md +38 -0
- package/knowledge/negocios/roi-e-unit-economics.md +46 -0
- package/knowledge/pipeline/1-descobrir.md +26 -0
- package/knowledge/pipeline/2-estrategizar.md +26 -0
- package/knowledge/pipeline/3-estruturar.md +27 -0
- package/knowledge/pipeline/4-construir.md +27 -0
- package/knowledge/pipeline/5-endurecer.md +28 -0
- package/knowledge/pipeline/6-lancar.md +27 -0
- package/knowledge/pipeline/7-operar.md +27 -0
- package/knowledge/security/lgpd-conformidade-basica.md +35 -0
- package/knowledge/security/owasp-secure-coding-gates.md +220 -0
- package/knowledge/security/owasp-top10-threat-assessment.md +287 -0
- package/knowledge/security/threat-modeling-stride.md +34 -0
- package/knowledge/web-craft/a11y-audit-checklist.md +251 -0
- package/knowledge/web-craft/accessible-component-patterns.md +383 -0
- package/knowledge/web-craft/anti-ai-look.md +114 -0
- package/knowledge/web-craft/design-system-from-code.md +195 -0
- package/knowledge/web-craft/intrinsic-css-layout.md +420 -0
- package/knowledge/web-craft/style-cloning.md +185 -0
- package/knowledge/web-craft/visual-polish-review.md +183 -0
- package/package.json +55 -0
- package/runbooks/campanha-de-conteudo.md +36 -0
- package/runbooks/feature-em-projeto-existente.md +37 -0
- package/runbooks/mvp-startup.md +38 -0
- package/runbooks/resposta-a-incidente.md +37 -0
- package/squads/exemplo-conteudo/agents/editor-chefe.md +48 -0
- package/squads/exemplo-conteudo/agents/pesquisador.md +44 -0
- package/squads/exemplo-conteudo/agents/redator.md +45 -0
- package/squads/exemplo-conteudo/knowledge/estilo-editorial.md +21 -0
- package/squads/exemplo-conteudo/squad.yaml +19 -0
- package/squads/exemplo-conteudo/tasks/pesquisar-fontes.md +26 -0
- package/squads/exemplo-conteudo/tasks/planejar-pauta.md +27 -0
- package/squads/exemplo-conteudo/tasks/redigir-artigo.md +26 -0
- package/squads/exemplo-conteudo/tasks/revisar-artigo.md +27 -0
- package/squads/marketing/agents/analista.md +56 -0
- package/squads/marketing/agents/chefe-marketing.md +65 -0
- package/squads/marketing/agents/conteudo.md +55 -0
- package/squads/marketing/agents/copy.md +55 -0
- package/squads/marketing/agents/growth.md +56 -0
- package/squads/marketing/agents/social.md +55 -0
- package/squads/marketing/squad.yaml +17 -0
- package/squads/marketing/tasks/aprovar-campanha.md +43 -0
- package/squads/negocios/agents/chefe-negocios.md +65 -0
- package/squads/negocios/agents/financas-roi.md +55 -0
- package/squads/negocios/agents/suporte.md +55 -0
- package/squads/negocios/agents/vendas-proposta.md +56 -0
- package/squads/negocios/squad.yaml +17 -0
- package/squads/negocios/tasks/aprovar-proposta.md +40 -0
- package/squads/security/agents/appsec-reviewer.md +59 -0
- package/squads/security/agents/chefe-seguranca.md +65 -0
- package/squads/security/agents/compliance-auditor.md +60 -0
- package/squads/security/agents/threat-modeler.md +60 -0
- package/squads/security/squad.yaml +20 -0
- package/squads/security/tasks/aprovar-gate-seguranca.md +42 -0
- package/squads/security/tasks/emitir-parecer-conformidade.md +42 -0
- package/tasks/README.md +72 -0
- package/tasks/accessibility-wcag-checklist.md +69 -0
- package/tasks/advanced-elicitation.md +42 -0
- package/tasks/analyze-performance.md +54 -0
- package/tasks/analyze-project-structure.md +59 -0
- package/tasks/apply-qa-fixes.md +57 -0
- package/tasks/architect-analyze-impact.md +62 -0
- package/tasks/archive-squad.md +52 -0
- package/tasks/audit-codebase.md +53 -0
- package/tasks/build-component.md +61 -0
- package/tasks/calculate-roi.md +63 -0
- package/tasks/ci-cd-configuration.md +51 -0
- package/tasks/collect-visual-evidence.md +62 -0
- package/tasks/compose-molecule.md +57 -0
- package/tasks/consolidate-patterns.md +54 -0
- package/tasks/create-brownfield-prd.md +54 -0
- package/tasks/create-competitor-analysis.md +42 -0
- package/tasks/create-deep-research-prompt.md +62 -0
- package/tasks/create-doc.md +62 -0
- package/tasks/create-epic.md +49 -0
- package/tasks/create-front-end-spec.md +56 -0
- package/tasks/create-migration-plan.md +57 -0
- package/tasks/create-next-story.md +66 -0
- package/tasks/create-prd.md +53 -0
- package/tasks/create-project-brief.md +47 -0
- package/tasks/create-rls-policies.md +59 -0
- package/tasks/create-schema.md +57 -0
- package/tasks/create-service.md +55 -0
- package/tasks/create-squad.md +100 -0
- package/tasks/create-suite.md +62 -0
- package/tasks/db-apply-migration.md +56 -0
- package/tasks/db-domain-modeling.md +57 -0
- package/tasks/db-dry-run.md +50 -0
- package/tasks/db-env-check.md +57 -0
- package/tasks/db-load-csv.md +54 -0
- package/tasks/db-policy-apply.md +58 -0
- package/tasks/db-rollback.md +51 -0
- package/tasks/db-run-sql.md +61 -0
- package/tasks/db-seed.md +52 -0
- package/tasks/db-smoke-test.md +51 -0
- package/tasks/db-snapshot.md +48 -0
- package/tasks/db-verify-order.md +49 -0
- package/tasks/deliberate.md +46 -0
- package/tasks/design-indexes.md +59 -0
- package/tasks/dev-develop-story.md +61 -0
- package/tasks/document-project.md +59 -0
- package/tasks/execute-checklist.md +57 -0
- package/tasks/execute-epic-plan.md +52 -0
- package/tasks/execute-subtask.md +51 -0
- package/tasks/extend-pattern.md +63 -0
- package/tasks/extend-squad.md +60 -0
- package/tasks/extract-patterns.md +64 -0
- package/tasks/extract-tokens.md +59 -0
- package/tasks/facilitate-brainstorming-session.md +42 -0
- package/tasks/generate-ai-frontend-prompt.md +57 -0
- package/tasks/generate-documentation.md +60 -0
- package/tasks/generate-migration-strategy.md +57 -0
- package/tasks/generate-shock-report.md +56 -0
- package/tasks/mcp-management.md +66 -0
- package/tasks/orchestrate.md +50 -0
- package/tasks/perform-market-research.md +42 -0
- package/tasks/plan-create-context.md +57 -0
- package/tasks/plan-create-implementation.md +58 -0
- package/tasks/po-close-story.md +60 -0
- package/tasks/po-manage-story-backlog.md +59 -0
- package/tasks/po-pull-story.md +60 -0
- package/tasks/po-sync-story.md +59 -0
- package/tasks/pr-automation.md +50 -0
- package/tasks/pre-push-quality-gate.md +54 -0
- package/tasks/push.md +53 -0
- package/tasks/qa-browser-console-check.md +52 -0
- package/tasks/qa-create-fix-request.md +58 -0
- package/tasks/qa-evidence-requirements.md +55 -0
- package/tasks/qa-false-positive-detection.md +55 -0
- package/tasks/qa-fix-issues.md +55 -0
- package/tasks/qa-gate.md +53 -0
- package/tasks/qa-migration-validation.md +58 -0
- package/tasks/qa-nfr-assess.md +45 -0
- package/tasks/qa-review-story.md +56 -0
- package/tasks/qa-risk-profile.md +45 -0
- package/tasks/qa-security-checklist.md +64 -0
- package/tasks/qa-test-design.md +47 -0
- package/tasks/qa-trace-requirements.md +48 -0
- package/tasks/release-management.md +53 -0
- package/tasks/repository-cleanup.md +61 -0
- package/tasks/route.md +44 -0
- package/tasks/run-tests.md +50 -0
- package/tasks/security-audit.md +54 -0
- package/tasks/setup-database.md +60 -0
- package/tasks/setup-design-system.md +60 -0
- package/tasks/shard-doc.md +60 -0
- package/tasks/spec-assess-complexity.md +55 -0
- package/tasks/spec-critique.md +64 -0
- package/tasks/spec-gather-requirements.md +48 -0
- package/tasks/spec-research-dependencies.md +42 -0
- package/tasks/spec-write-spec.md +50 -0
- package/tasks/test-as-user.md +52 -0
- package/tasks/ux-create-wireframe.md +54 -0
- package/tasks/ux-user-research.md +55 -0
- package/tasks/validate-next-story.md +61 -0
- package/tasks/validate-squad.md +55 -0
- package/tasks/verify-subtask.md +52 -0
- package/tasks/version-management.md +45 -0
- package/templates/README.md +47 -0
- package/templates/architecture-tmpl.md +115 -0
- package/templates/competitor-analysis-tmpl.md +87 -0
- package/templates/epic-tmpl.md +83 -0
- package/templates/front-end-spec-tmpl.md +110 -0
- package/templates/market-research-tmpl.md +98 -0
- package/templates/migration-plan-tmpl.md +92 -0
- package/templates/prd-tmpl.md +95 -0
- package/templates/project-brief-tmpl.md +100 -0
- package/templates/qa-verdict-tmpl.md +73 -0
- package/templates/rls-policies-tmpl.md +93 -0
- package/templates/schema-design-tmpl.md +107 -0
- package/templates/spec-tmpl.md +88 -0
- package/templates/squad/agent-dna-tmpl.md +72 -0
- package/templates/squad/chief-dna-tmpl.md +98 -0
- package/templates/squad/squad-task-tmpl.md +50 -0
- package/templates/squad/squad-yaml-tmpl.md +47 -0
- package/templates/story-tmpl.md +63 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: distributed-patterns-cheatsheet
|
|
3
|
+
domain: architecture
|
|
4
|
+
agents: [architect]
|
|
5
|
+
when: "ao escolher protocolos, comunicação e padrões de resiliência num sistema distribuído"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Distributed Patterns — cheatsheet de decisão "use X quando Y"
|
|
9
|
+
|
|
10
|
+
## O problema
|
|
11
|
+
|
|
12
|
+
A maioria das arquiteturas distribuídas erra não por falta de conhecimento, mas por **default não justificado**: escolhe-se REST porque é o que se conhece, retry porque "é seguro", blue-green porque soa robusto — sem amarrar a escolha ao critério que a justifica. O resultado é previsível: gRPC interno que ninguém consegue debugar no browser, retries que amplificam um incidente em vez de absorvê-lo, rate limiter que deixa passar o dobro do limite na virada da janela, deploy que custa 2x sem precisar.
|
|
13
|
+
|
|
14
|
+
Este pack é uma **régua de decisão**. Cada item tem o **critério de escolha** (o "quando Y") e o **trade-off** que você está aceitando. Se você não consegue nomear o Y, não escolha o X — o default está te traindo.
|
|
15
|
+
|
|
16
|
+
## Os princípios / o conhecimento
|
|
17
|
+
|
|
18
|
+
### 1. Estilo de API — REST vs GraphQL vs gRPC vs WebSocket
|
|
19
|
+
|
|
20
|
+
A confusão comum é tratá-los como concorrentes. **São complementos.** O mapa mental sólido: REST para API pública, gRPC para serviço interno, GraphQL para necessidade de dados orientada ao cliente, WebSocket para tempo real bidirecional.
|
|
21
|
+
|
|
22
|
+
| Estilo | Transporte / formato | Use quando | Trade-off que você aceita |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| **REST** | HTTP/1.1 + JSON | API pública, integração externa, recursos CRUD, quando cache de infra (CDN/proxy) importa | Over/under-fetching; múltiplos round-trips para dados relacionados |
|
|
25
|
+
| **GraphQL** | HTTP + schema/query | Cliente decide o shape dos dados; agrega N fontes num gateway; payloads onde se pega só o necessário | Cache HTTP fraco (POST único); complexidade de servidor; risco de query cara N+1 |
|
|
26
|
+
| **gRPC** | HTTP/2 + Protobuf (binário, IDL) | Comunicação serviço-a-serviço interna, baixa latência, contratos fortes, streaming | **Não roda no browser** (XHR não fala HTTP/2 puro); binário não é human-readable; sem cache HTTP nativo |
|
|
27
|
+
| **WebSocket** | TCP full-duplex, conexão longa | Tempo real bidirecional (chat, presença, dados ao vivo) entre muitos clientes e o servidor | Conexão persistente consome recurso do servidor; escala horizontal exige sticky session / estado distribuído |
|
|
28
|
+
|
|
29
|
+
**Números reais para calibrar:**
|
|
30
|
+
- gRPC é tipicamente **5 a 10x mais rápido que REST** em benchmarks — vem do HTTP/2 (multiplexing de várias requests numa conexão) + Protobuf binário.
|
|
31
|
+
- WebSocket ganha em **latência para mensagens pequenas e frequentes** porque elimina o handshake repetido — exatamente onde REST sangra.
|
|
32
|
+
|
|
33
|
+
**Tells de escolha errada:**
|
|
34
|
+
- gRPC exposto direto ao browser → precisa de gRPC-Web/proxy; sinal de que REST ou GraphQL era o certo na borda.
|
|
35
|
+
- WebSocket para request-response pontual → você pagou conexão persistente por nada.
|
|
36
|
+
- GraphQL numa API com 3 endpoints estáveis → complexidade sem retorno; REST resolvia.
|
|
37
|
+
|
|
38
|
+
### 2. API Gateway — a borda única
|
|
39
|
+
|
|
40
|
+
O gateway é o ponto de entrada que faz o que cada serviço não deve repetir: **autenticação/autorização, rate limiting, roteamento, agregação de respostas, terminação TLS, observabilidade**. Use quando há múltiplos serviços atrás de uma fachada e você quer cross-cutting concerns num lugar só.
|
|
41
|
+
|
|
42
|
+
| Responsabilidade | Por que no gateway, não no serviço |
|
|
43
|
+
|---|---|
|
|
44
|
+
| AuthN / AuthZ | Não duplicar verificação de token em N serviços |
|
|
45
|
+
| Rate limiting | Proteger o backend inteiro de um cliente abusivo |
|
|
46
|
+
| Roteamento / versionamento | Desacoplar URL pública da topologia interna |
|
|
47
|
+
| Agregação | Compor resposta de vários serviços (BFF) numa chamada |
|
|
48
|
+
| TLS termination / observabilidade | Ponto único de métricas, logs, tracing |
|
|
49
|
+
|
|
50
|
+
**Trade-off:** o gateway é um **single point of failure** e ponto de latência — exige HA e cuidado para não virar monolito disfarçado. Não coloque lógica de negócio nele.
|
|
51
|
+
|
|
52
|
+
### 3. Síncrono vs Assíncrono
|
|
53
|
+
|
|
54
|
+
| Critério | Síncrono (request-response) | Assíncrono (mensagem/evento) |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Acoplamento temporal | Forte — chamador espera | Fraco — fire-and-forget |
|
|
57
|
+
| Use quando | Precisa da resposta agora para continuar (consulta, validação) | Pode processar depois; desacoplar produtor e consumidor; absorver picos |
|
|
58
|
+
| Falha do downstream | Propaga imediatamente ao chamador | Absorvida pela fila; reprocessa depois |
|
|
59
|
+
| Trade-off | Cascata de latência e de falha entre serviços | Complexidade: eventual consistency, ordenação, idempotência obrigatória |
|
|
60
|
+
|
|
61
|
+
Regra prática: **toda cadeia síncrona longa é uma cascata de falha esperando acontecer.** Se a operação não precisa do resultado em linha, torne-a assíncrona.
|
|
62
|
+
|
|
63
|
+
### 4. Message Queue vs Event Streaming
|
|
64
|
+
|
|
65
|
+
Ambos são assíncronos, mas resolvem coisas diferentes. Confundir os dois é um erro clássico.
|
|
66
|
+
|
|
67
|
+
| Dimensão | Message Queue (ex.: RabbitMQ, SQS) | Event Streaming (ex.: Kafka) |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Modelo | Fila: mensagem **consumida e removida** | Log append-only: evento **retido e re-lido** |
|
|
70
|
+
| Consumidores | Tipicamente 1 consumidor por mensagem (work queue) | N consumidores independentes, cada um com seu offset |
|
|
71
|
+
| Replay | Não (mensagem some após ack) | Sim (re-processa do offset, retém por tempo/tamanho) |
|
|
72
|
+
| Ordem | Por fila | Por partição |
|
|
73
|
+
| Use quando | Distribuir tarefas/jobs, desacoplar work, balancear carga | Pipeline de eventos, event sourcing, múltiplos consumidores, auditoria/replay, alto throughput |
|
|
74
|
+
| Trade-off | Sem histórico; difícil ter múltiplas views do mesmo evento | Mais operacional (partições, offsets, retenção); ordem só dentro da partição |
|
|
75
|
+
|
|
76
|
+
**Por que Kafka é rápido (para citar):** escrita sequencial em disco (append-only), zero-copy no envio ao consumidor, e batching — evita o custo de I/O aleatório.
|
|
77
|
+
|
|
78
|
+
### 5. Idempotência
|
|
79
|
+
|
|
80
|
+
Idempotência é a **pré-condição de qualquer retry seguro**. Operação idempotente = executá-la N vezes tem o mesmo efeito de executá-la uma vez. Sem isso, retry vira cobrança duplicada.
|
|
81
|
+
|
|
82
|
+
**Top casos onde aplicar idempotência:**
|
|
83
|
+
1. **Pagamentos / cobrança** — retry não pode cobrar duas vezes.
|
|
84
|
+
2. **Criação de recurso** (POST que cria pedido) — request duplicado não cria dois pedidos.
|
|
85
|
+
3. **Consumo de mensagem** (at-least-once delivery) — a mesma mensagem pode chegar 2x.
|
|
86
|
+
4. **Webhooks** — provedores reenviam em caso de timeout.
|
|
87
|
+
5. **Retries automáticos** entre serviços.
|
|
88
|
+
6. **Operações disparadas por usuário** (double-click no "Comprar").
|
|
89
|
+
|
|
90
|
+
**Como implementar:** **idempotency key** — o cliente envia um ID único (header `Idempotency-Key`); o servidor guarda o resultado da primeira execução e, em requests repetidos com a mesma key, retorna o resultado cacheado em vez de re-executar.
|
|
91
|
+
|
|
92
|
+
### 6. Retries — estratégias
|
|
93
|
+
|
|
94
|
+
Retry resolve **falha transitória** (timeout momentâneo, blip de rede). Não resolve serviço caído — para isso é circuit breaker.
|
|
95
|
+
|
|
96
|
+
| Estratégia | Comportamento | Risco |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| **Retry imediato** | Tenta de novo na hora | Pode martelar serviço já estressado |
|
|
99
|
+
| **Intervalo fixo** | Espera X ms entre tentativas | Retries sincronizados de N clientes = thundering herd |
|
|
100
|
+
| **Exponential backoff** | Atraso cresce a cada tentativa (1s, 2s, 4s, 8s…) | Reduz pressão, mas ainda sincroniza se todos começam juntos |
|
|
101
|
+
| **Backoff + jitter** | Backoff exponencial **com aleatoriedade** no intervalo | **Recomendado** — espalha os retries, evita retry storm / thundering herd |
|
|
102
|
+
|
|
103
|
+
**Regras invioláveis do retry:**
|
|
104
|
+
- **Limite o número** de tentativas (bounded) — retry infinito amplifica incidente.
|
|
105
|
+
- **Só retentar operação idempotente** — senão duplica efeito colateral.
|
|
106
|
+
- **Instrumente** — sem métrica, retries escondem o incidente em vez de absorvê-lo.
|
|
107
|
+
- **Só para transitório** — se a falha é persistente, retry só piora.
|
|
108
|
+
|
|
109
|
+
### 7. Resiliência — Circuit Breaker
|
|
110
|
+
|
|
111
|
+
O circuit breaker protege contra **falha não-transitória** (serviço down): para de chamar o downstream para não desperdiçar recurso e dar fôlego pra ele se recuperar. Três estados:
|
|
112
|
+
|
|
113
|
+
| Estado | Comportamento | Transição |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| **CLOSED** | Operação normal; todas as requests passam | Falhas acima do threshold → OPEN |
|
|
116
|
+
| **OPEN** | Bloqueia tudo; **falha imediata** sem chamada de rede | Após timeout configurado (tipicamente 30s a poucos minutos) → HALF-OPEN |
|
|
117
|
+
| **HALF-OPEN** | Deixa passar **1 ou poucas** requests de teste | Sucesso → CLOSED; falha → volta a OPEN e reinicia o timer |
|
|
118
|
+
|
|
119
|
+
**Circuit breaker vs Retry — quando cada um:**
|
|
120
|
+
|
|
121
|
+
| | Retry | Circuit Breaker |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| Trata | Falha **transitória** (blip momentâneo) | Falha **persistente** (serviço provavelmente down) |
|
|
124
|
+
| Efeito | Tenta de novo | Para de tentar para não piorar |
|
|
125
|
+
| Juntos | Retry **dentro** do CLOSED; o breaker corta o retry quando vira OPEN | Complementares, não substitutos |
|
|
126
|
+
|
|
127
|
+
Combine com **bulkhead** (isolar thread pools para que um downstream lento não consuma todas as threads) e **timeout** (toda chamada remota tem prazo).
|
|
128
|
+
|
|
129
|
+
### 8. Resiliência — Rate Limiting (os 5 algoritmos)
|
|
130
|
+
|
|
131
|
+
Escolher o algoritmo errado custa caro: ou você deixa passar burst que derruba o backend (boundary exploit), ou gasta memória demais para precisão que não precisa.
|
|
132
|
+
|
|
133
|
+
| Algoritmo | Como funciona | Use quando | Trade-off |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| **Token Bucket** | Bucket enche tokens a taxa fixa; cada request consome 1; bucket = capacidade de burst | **Default para API pública** — modela burst separado da taxa sustentada; permite picos controlados reais | Precisa afinar tamanho do bucket vs taxa de refill |
|
|
136
|
+
| **Leaky Bucket** | Fila drena a taxa **constante**; request entra na fila, rejeitado se cheia | Saída suave e constante, independente do burst de entrada (proteger downstream que odeia picos) | Enfileira → adiciona latência; descarta na fila cheia |
|
|
137
|
+
| **Fixed Window Counter** | Conta requests por janela fixa (ex.: por minuto) | Simplicidade máxima; throttle de login, limites básicos, serviço interno onde aproximação serve | **Boundary exploit**: na virada da janela pode passar até 2x o limite |
|
|
138
|
+
| **Sliding Window Log** | Guarda timestamp **exato** de cada request recente | Precisão máxima e auditoria — pagamento, autenticação, endpoint que exige contagem exata | Maior custo de memória e operação (guarda cada timestamp) |
|
|
139
|
+
| **Sliding Window Counter** | Aproxima a janela deslizante ponderando janela atual + anterior | **Melhor compromisso em escala distribuída** — reduz o boundary burst com custo baixo, alto throughput | Aproximação (não tão exato quanto o log), mas bom o suficiente |
|
|
140
|
+
|
|
141
|
+
**Regra de bolso:** API pública → **token bucket**. Precisa de exatidão/auditoria → **sliding window log**. Escala distribuída com eficiência → **sliding window counter**. Saída precisa ser perfeitamente suave → **leaky bucket**. Só simplicidade e tolera burst de borda → **fixed window**.
|
|
142
|
+
|
|
143
|
+
### 9. Estratégias de Deploy
|
|
144
|
+
|
|
145
|
+
| Estratégia | Como | Downtime | Rollback | Risco | Custo infra | Complexidade | Use quando |
|
|
146
|
+
|---|---|---|---|---|---|---|---|
|
|
147
|
+
| **Rolling** | Substitui instâncias aos poucos | Não | **Lento** | Médio | 1x–1.25x | Baixa | Bug fixes de rotina; default barato |
|
|
148
|
+
| **Blue-Green** | Dois ambientes idênticos; troca **todo** o tráfego de uma vez | Não | **Instantâneo** | Baixo | **2x** | Média | Patch crítico de segurança; release que exige rollback imediato |
|
|
149
|
+
| **Canary** | Roteia % pequeno (ex.: 25%) pro novo, observa, então expande | Não | **Rápido** | Baixo | 1x–1.1x | **Alta** | Release de feature grande; quer validar com tráfego real antes do full rollout |
|
|
150
|
+
| **A/B Testing** | Variantes para medir comportamento (experimento, não mitigação) | Não | Rápido | **Maior** (sem rollback por erro/latência automático) | 1x–1.1x | Média | Experimento de produto; ambas versões já estáveis |
|
|
151
|
+
| **Shadow** | Espelha tráfego real pro novo **sem retornar resposta ao usuário** | Não | N/A | Nenhum (usuário não afetado) | 2x | Alta | Validar código novo com workload real sem impacto no usuário |
|
|
152
|
+
|
|
153
|
+
**Feature flags são ortogonais:** desacoplam **deploy de release**. Você faz deploy do código com qualquer estratégia, mas mantém a feature desligada e liga via config — gradualmente, sem novo deploy.
|
|
154
|
+
|
|
155
|
+
**Critério de escolha resumido:** bug fix rotineiro → rolling; feature grande → canary; patch crítico que precisa de reversão instantânea → blue-green; quer medir comportamento → A/B; quer testar sob carga real sem risco → shadow.
|
|
156
|
+
|
|
157
|
+
## Checklist
|
|
158
|
+
|
|
159
|
+
Antes de fechar a arquitetura, responda — cada "não" é um default não justificado a revisar:
|
|
160
|
+
|
|
161
|
+
- [ ] Para cada API, eu sei nomear **por que** REST/GraphQL/gRPC/WebSocket e não o outro?
|
|
162
|
+
- [ ] Tem gRPC que precisa ser consumido pelo browser? (vai precisar de proxy/gRPC-Web)
|
|
163
|
+
- [ ] Cross-cutting concerns (auth, rate limit, TLS) estão no **gateway**, não duplicados nos serviços?
|
|
164
|
+
- [ ] As cadeias síncronas longas foram revisadas — o que pode ser assíncrono virou assíncrono?
|
|
165
|
+
- [ ] A escolha fila vs streaming bate com a necessidade de **replay / múltiplos consumidores**?
|
|
166
|
+
- [ ] Toda operação retentável é **idempotente** (idempotency key onde há efeito colateral)?
|
|
167
|
+
- [ ] Os retries têm **limite**, usam **backoff + jitter**, e são **só para falha transitória**?
|
|
168
|
+
- [ ] Chamadas a downstream têm **timeout + circuit breaker** (e bulkhead onde compartilham pool)?
|
|
169
|
+
- [ ] O algoritmo de rate limiting foi escolhido pelo critério (token bucket default; log p/ exatidão; counter p/ escala)?
|
|
170
|
+
- [ ] A estratégia de deploy bate com o **custo de rollback** aceitável (instantâneo → blue-green; gradual → canary)?
|
|
171
|
+
- [ ] Feature flags separam **deploy de release** onde faz sentido?
|
|
172
|
+
|
|
173
|
+
## Tabela de decisão "use X quando Y"
|
|
174
|
+
|
|
175
|
+
| Decisão (X) | Escolha quando (Y) | Trade-off aceito |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| **REST** | API pública/externa, CRUD, cache de CDN importa | Over/under-fetching, round-trips |
|
|
178
|
+
| **GraphQL** | Cliente dita o shape; agrega N fontes | Cache HTTP fraco, risco N+1 |
|
|
179
|
+
| **gRPC** | Serviço↔serviço interno, baixa latência, contrato forte | Sem browser nativo, binário, sem cache HTTP |
|
|
180
|
+
| **WebSocket** | Tempo real bidirecional, muitas conexões vivas | Recurso por conexão, sticky sessions na escala |
|
|
181
|
+
| **API Gateway** | N serviços atrás de uma borda com cross-cutting concerns | SPOF + latência; exige HA |
|
|
182
|
+
| **Assíncrono** | Não precisa da resposta em linha; absorver picos | Eventual consistency, idempotência obrigatória |
|
|
183
|
+
| **Message Queue** | Distribuir jobs, 1 consumidor por mensagem | Sem replay/histórico |
|
|
184
|
+
| **Event Streaming** | Múltiplos consumidores, replay, auditoria, alto throughput | Ops de partição/offset/retenção |
|
|
185
|
+
| **Idempotency key** | Qualquer operação com efeito colateral que pode ser retentada | Estado extra para guardar resultados |
|
|
186
|
+
| **Backoff + jitter** | Retry de falha transitória sob concorrência | Latência adicional na recuperação |
|
|
187
|
+
| **Circuit Breaker** | Downstream provavelmente down (falha persistente) | Falha rápida temporária enquanto OPEN |
|
|
188
|
+
| **Token Bucket** | Rate limit de API pública com burst real | Afinar bucket vs refill |
|
|
189
|
+
| **Sliding Window Log** | Rate limit com exatidão/auditoria (pagamento, auth) | Custo de memória por timestamp |
|
|
190
|
+
| **Sliding Window Counter** | Rate limit em escala distribuída eficiente | Aproximação (não exato) |
|
|
191
|
+
| **Leaky Bucket** | Saída precisa ser constante/suave pro downstream | Latência de fila; descarte na fila cheia |
|
|
192
|
+
| **Fixed Window** | Throttle simples, tolera burst de borda | Boundary exploit (até 2x na virada) |
|
|
193
|
+
| **Rolling deploy** | Bug fix de rotina, custo baixo | Rollback lento |
|
|
194
|
+
| **Blue-Green** | Patch crítico, rollback instantâneo necessário | Custo de infra 2x |
|
|
195
|
+
| **Canary** | Feature grande, validar com tráfego real | Alta complexidade de pipeline |
|
|
196
|
+
| **Shadow** | Testar sob carga real sem risco ao usuário | Custo 2x, sem resposta ao usuário |
|
|
197
|
+
| **Feature flags** | Separar deploy de release; rollout gradual por config | Gestão de flags / débito se não limpar |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
> **Fonte de referência:** ByteByteGo — *system-design-101* (github.com/ByteByteGoHq/system-design-101), seções "SOAP vs REST vs GraphQL vs RPC", "What is gRPC?", "API Gateway 101", "Types of Message Queues", "Kafka 101 / Why is Kafka Fast?", "Top 6 Cases to Apply Idempotency", "Retry Strategies for System Failures", "Resiliency Patterns", "Top 5 Most-Used Deployment Strategies". Números e critérios consolidados com fontes públicas de engenharia (rate limiting, circuit breaker, deployment trade-offs).
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: saas-subscription-blueprint
|
|
3
|
+
domain: architecture
|
|
4
|
+
agents: [architect]
|
|
5
|
+
when: "ao construir um SaaS que cobra assinatura (do zero ao faturamento)"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SaaS de assinatura — do zero ao primeiro faturamento
|
|
9
|
+
|
|
10
|
+
Blueprint extraído do `nextjs/saas-starter` (Vercel) — o starter oficial com Next.js (App Router),
|
|
11
|
+
Postgres, Drizzle, Stripe e shadcn/ui. Tudo aqui é **grounded no código real** do template, não em
|
|
12
|
+
"boas práticas" abstratas. O objetivo é dar ao arquiteto o caminho concreto: schema, fluxo de cobrança,
|
|
13
|
+
auth, RBAC e os pontos onde o starter falha de propósito (e você não pode copiar cego).
|
|
14
|
+
|
|
15
|
+
## O problema
|
|
16
|
+
|
|
17
|
+
"Fazer um SaaS" parece um épico, mas o caminho comercial mínimo é estreito e conhecido: **alguém se
|
|
18
|
+
cadastra, vira dono de um team, escolhe um plano, paga no Stripe, e o estado da assinatura volta pro seu
|
|
19
|
+
banco via webhook.** A maioria das tentativas erra em um de quatro lugares:
|
|
20
|
+
|
|
21
|
+
1. **Modelam billing no usuário, não no team.** Aí quando o cliente quer convidar um colega, o plano não
|
|
22
|
+
acompanha — assinatura é por *conta de cobrança* (team), não por pessoa.
|
|
23
|
+
2. **Confiam no redirect de sucesso do Checkout pra liberar acesso.** O redirect é cosmético e burlável;
|
|
24
|
+
a fonte de verdade do estado da assinatura é **o webhook**, não a URL de retorno.
|
|
25
|
+
3. **Reinventam o portal de billing** (trocar cartão, cancelar, fazer upgrade) — quando o Stripe Customer
|
|
26
|
+
Portal já faz isso hospedado e PCI-compliant.
|
|
27
|
+
4. **Esquecem o RBAC.** Qualquer membro consegue remover o dono ou convidar gente. O próprio starter tem
|
|
28
|
+
esse buraco — é o tell número 1 de SaaS copiado de template.
|
|
29
|
+
|
|
30
|
+
Este pack resolve os quatro.
|
|
31
|
+
|
|
32
|
+
## O conhecimento
|
|
33
|
+
|
|
34
|
+
### 1. Modelagem: a unidade de cobrança é o `team`, não o `user`
|
|
35
|
+
|
|
36
|
+
O schema real do starter (Drizzle / Postgres). Note onde os campos do Stripe moram — **em `teams`**:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// users — identidade e auth
|
|
40
|
+
users {
|
|
41
|
+
id serial PK
|
|
42
|
+
name varchar(100)
|
|
43
|
+
email varchar(255) NOT NULL UNIQUE
|
|
44
|
+
passwordHash text NOT NULL
|
|
45
|
+
role varchar(20) NOT NULL DEFAULT 'member' // role GLOBAL, não de team
|
|
46
|
+
createdAt timestamp NOT NULL DEFAULT now()
|
|
47
|
+
updatedAt timestamp NOT NULL DEFAULT now()
|
|
48
|
+
deletedAt timestamp // soft delete
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// teams — a CONTA DE COBRANÇA. Stripe vive aqui.
|
|
52
|
+
teams {
|
|
53
|
+
id serial PK
|
|
54
|
+
name varchar(100) NOT NULL
|
|
55
|
+
createdAt timestamp NOT NULL DEFAULT now()
|
|
56
|
+
updatedAt timestamp NOT NULL DEFAULT now()
|
|
57
|
+
stripeCustomerId text UNIQUE // 1 customer Stripe por team
|
|
58
|
+
stripeSubscriptionId text UNIQUE
|
|
59
|
+
stripeProductId text
|
|
60
|
+
planName varchar(50)
|
|
61
|
+
subscriptionStatus varchar(20) // espelho do status do Stripe
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// teamMembers — a relação N:N + a role DE TEAM (esta é a que importa pro RBAC)
|
|
65
|
+
teamMembers {
|
|
66
|
+
id serial PK
|
|
67
|
+
userId integer NOT NULL FK -> users.id
|
|
68
|
+
teamId integer NOT NULL FK -> teams.id
|
|
69
|
+
role varchar(50) NOT NULL // 'owner' | 'member'
|
|
70
|
+
joinedAt timestamp NOT NULL DEFAULT now()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// invitations — convite pendente por e-mail, aceito no signup
|
|
74
|
+
invitations {
|
|
75
|
+
id serial PK
|
|
76
|
+
teamId integer NOT NULL FK -> teams.id
|
|
77
|
+
email varchar(255) NOT NULL
|
|
78
|
+
role varchar(50) NOT NULL
|
|
79
|
+
invitedBy integer NOT NULL FK -> users.id
|
|
80
|
+
invitedAt timestamp NOT NULL DEFAULT now()
|
|
81
|
+
status varchar(20) NOT NULL DEFAULT 'pending' // pending | accepted
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// activityLogs — trilha de auditoria append-only
|
|
85
|
+
activityLogs {
|
|
86
|
+
id serial PK
|
|
87
|
+
teamId integer NOT NULL FK -> teams.id
|
|
88
|
+
userId integer FK -> users.id // nullable (eventos do sistema)
|
|
89
|
+
action text NOT NULL // valor do enum ActivityType
|
|
90
|
+
timestamp timestamp NOT NULL DEFAULT now()
|
|
91
|
+
ipAddress varchar(45) // 45 = caber IPv6
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Decisão concreta a internalizar:** existem **duas roles** e elas não são a mesma coisa.
|
|
96
|
+
`users.role` é global (útil pra super-admin do produto). `teamMembers.role` é por team (`owner`/`member`)
|
|
97
|
+
— **é esta que governa quem pode convidar, remover e gerenciar billing.** Confundir as duas é como o RBAC
|
|
98
|
+
quebra silenciosamente.
|
|
99
|
+
|
|
100
|
+
O enum de auditoria (`ActivityType`), com os 10 eventos que o starter rastreia:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
SIGN_UP · SIGN_IN · SIGN_OUT · UPDATE_PASSWORD · UPDATE_ACCOUNT · DELETE_ACCOUNT
|
|
104
|
+
CREATE_TEAM · INVITE_TEAM_MEMBER · REMOVE_TEAM_MEMBER · ACCEPT_INVITATION
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2. Cobrança: Checkout (hospedado) → webhook (fonte de verdade) → Customer Portal
|
|
108
|
+
|
|
109
|
+
O fluxo tem três peças, e cada uma tem uma responsabilidade que não se mistura.
|
|
110
|
+
|
|
111
|
+
**a) `createCheckoutSession` — manda o cliente pro Checkout hospedado do Stripe.** Parâmetros reais:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
stripe.checkout.sessions.create({
|
|
115
|
+
mode: 'subscription', // assinatura, não pagamento único
|
|
116
|
+
payment_method_types: ['card'],
|
|
117
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
118
|
+
subscription_data: { trial_period_days: 14 }, // trial sem cartão preso na hora
|
|
119
|
+
allow_promotion_codes: true, // cupom na própria tela do Stripe
|
|
120
|
+
success_url: `${baseUrl}/api/stripe/checkout?session_id={CHECKOUT_SESSION_ID}`,
|
|
121
|
+
cancel_url: `${baseUrl}/pricing`,
|
|
122
|
+
// ...client_reference_id / customer = vínculo com o team
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
Se o usuário não está logado, o starter **redireciona pro sign-up antes** — não dá pra comprar anônimo
|
|
126
|
+
porque a assinatura precisa de um team pra ancorar.
|
|
127
|
+
|
|
128
|
+
**b) Webhook `POST /api/stripe/webhook` — a ÚNICA fonte de verdade do estado da assinatura.**
|
|
129
|
+
Verificação de assinatura obrigatória (senão qualquer um forja um POST e ganha plano premium):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const payload = await request.text(); // RAW body, não parseado
|
|
133
|
+
const signature = request.headers.get('stripe-signature') as string;
|
|
134
|
+
let event: Stripe.Event;
|
|
135
|
+
try {
|
|
136
|
+
event = stripe.webhooks.constructEvent(
|
|
137
|
+
payload, signature, process.env.STRIPE_WEBHOOK_SECRET!
|
|
138
|
+
);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return NextResponse.json(
|
|
141
|
+
{ error: 'Webhook signature verification failed.' }, { status: 400 }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
switch (event.type) {
|
|
146
|
+
case 'customer.subscription.updated':
|
|
147
|
+
case 'customer.subscription.deleted':
|
|
148
|
+
await handleSubscriptionChange(event.data.object as Stripe.Subscription);
|
|
149
|
+
break;
|
|
150
|
+
// outros eventos: loga e ignora (não falha)
|
|
151
|
+
}
|
|
152
|
+
return NextResponse.json({ received: true }); // 200 sempre que processou
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`handleSubscriptionChange` traduz o status do Stripe pro seu banco:
|
|
156
|
+
|
|
157
|
+
| Status do Stripe | O que grava em `teams` |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `active` ou `trialing` | preenche `stripeSubscriptionId`, `stripeProductId`, `planName`, `subscriptionStatus` |
|
|
160
|
+
| `canceled` ou `unpaid` | **zera** (`null`) `stripeSubscriptionId`, `stripeProductId`, `planName` |
|
|
161
|
+
|
|
162
|
+
**Por que isto importa:** o redirect de `success_url` é só UX. O acesso premium do cliente deve ser
|
|
163
|
+
decidido lendo `teams.subscriptionStatus` — que **só** é escrito pelo webhook. Em dev, o webhook chega via
|
|
164
|
+
`stripe listen --forward-to localhost:3000/api/stripe/webhook` (o CLI te dá o `whsec_...` pro
|
|
165
|
+
`STRIPE_WEBHOOK_SECRET`).
|
|
166
|
+
|
|
167
|
+
**c) `createCustomerPortalSession` — NÃO construa telas de billing.** O starter cria uma configuração de
|
|
168
|
+
portal hospedado do Stripe com:
|
|
169
|
+
- upgrade/downgrade de plano (troca de price, quantidade, cupom);
|
|
170
|
+
- atualização de método de pagamento;
|
|
171
|
+
- cancelamento com **motivo rastreado** (5 opções: `too_expensive`, `missing_features`,
|
|
172
|
+
`switched_service`, `unused`, `other`);
|
|
173
|
+
- proração habilitada.
|
|
174
|
+
|
|
175
|
+
Você manda o cliente pro portal e ele volta; o webhook `customer.subscription.updated` sincroniza o
|
|
176
|
+
resultado. Zero tela de cartão no seu código → zero escopo PCI no seu lado.
|
|
177
|
+
|
|
178
|
+
Leitura de catálogo (pra montar a pricing page a partir do Stripe, não hardcoded):
|
|
179
|
+
`getStripePrices()` retorna `{ id, productId, unitAmount, currency, interval, trialPeriodDays }`;
|
|
180
|
+
`getStripeProducts()` retorna `{ id, name, description, defaultPriceId }`.
|
|
181
|
+
|
|
182
|
+
### 3. Auth: JWT em cookie HttpOnly + middleware que renova a sessão
|
|
183
|
+
|
|
184
|
+
Sem provider externo de auth. Sessão é um JWT assinado (`jose`, `HS256`) guardado num cookie:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// session.ts
|
|
188
|
+
const SALT_ROUNDS = 10; // bcryptjs
|
|
189
|
+
hashPassword(pw) -> bcrypt hash
|
|
190
|
+
comparePasswords(pw, h) -> bool
|
|
191
|
+
|
|
192
|
+
signToken(payload) -> new SignJWT(payload)
|
|
193
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
194
|
+
.setIssuedAt()
|
|
195
|
+
.setExpirationTime('1 day from now')
|
|
196
|
+
.sign(key) // key = process.env.AUTH_SECRET
|
|
197
|
+
verifyToken(token) -> jwtVerify(token, key) // valida HS256
|
|
198
|
+
|
|
199
|
+
// cookie de sessão:
|
|
200
|
+
cookies().set('session', token, {
|
|
201
|
+
httpOnly: true, // JS do browser NÃO lê -> mitiga XSS roubar token
|
|
202
|
+
secure: true,
|
|
203
|
+
sameSite: 'lax', // mitiga CSRF mantendo navegação top-level
|
|
204
|
+
expires: <24h>,
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
O **middleware** protege rotas e **renova o token a cada GET** (sessão deslizante de 24h):
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// middleware.ts
|
|
212
|
+
const protectedRoutes = '/dashboard';
|
|
213
|
+
|
|
214
|
+
const sessionCookie = request.cookies.get('session');
|
|
215
|
+
const isProtectedRoute = pathname.startsWith(protectedRoutes);
|
|
216
|
+
|
|
217
|
+
if (isProtectedRoute && !sessionCookie) {
|
|
218
|
+
return NextResponse.redirect(new URL('/sign-in', request.url));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// em GET com sessão válida: re-assina com nova expiração (+24h) e regrava o cookie
|
|
222
|
+
// se verifyToken falhar: deleta o cookie e redireciona protegidas pro /sign-in
|
|
223
|
+
|
|
224
|
+
export const config = {
|
|
225
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
226
|
+
runtime: 'nodejs', // jose precisa de Node, não Edge runtime
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Decisão concreta:** o middleware faz só o *gate grosso* (tem cookie? rota protegida?). A autorização
|
|
231
|
+
fina (este usuário pode fazer *esta* ação?) **não** mora no middleware — mora na Server Action. Misturar os
|
|
232
|
+
dois é o erro clássico.
|
|
233
|
+
|
|
234
|
+
### 4. Server Actions: validação + autenticação como wrappers compostos
|
|
235
|
+
|
|
236
|
+
O starter encapsula o boilerplate de toda action em três wrappers. Use sempre o mais restritivo que serve:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
type ActionState = { error?: string; success?: string; [key: string]: any };
|
|
240
|
+
|
|
241
|
+
// 1. valida FormData contra um schema zod
|
|
242
|
+
validatedAction(schema, async (data, formData) => { ... })
|
|
243
|
+
|
|
244
|
+
// 2. valida + EXIGE usuário autenticado (injeta `user`)
|
|
245
|
+
validatedActionWithUser(schema, async (data, formData, user) => { ... })
|
|
246
|
+
|
|
247
|
+
// 3. exige user + carrega o team (injeta `team`, redireciona se não logado)
|
|
248
|
+
withTeam(async (formData, team) => { ... })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Padrão de validação real (`zod` + `safeParse`, devolve a 1ª mensagem de erro):
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const result = schema.safeParse(Object.fromEntries(formData));
|
|
255
|
+
if (!result.success) return { error: result.error.errors[0].message };
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 5. O buraco de RBAC que você NÃO pode copiar
|
|
259
|
+
|
|
260
|
+
Auditando as actions reais: `removeTeamMember` e `inviteTeamMember` usam `validatedActionWithUser` e
|
|
261
|
+
checam **apenas se o caller pertence a um team** — **não checam se ele é `owner`.** Isso significa que,
|
|
262
|
+
no template como está, **qualquer `member` pode remover qualquer pessoa (inclusive o dono) ou convidar
|
|
263
|
+
gente.** É um tell de SaaS feito de template cru.
|
|
264
|
+
|
|
265
|
+
O fix concreto que o arquiteto deve impor antes de qualquer mutação sensível:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// dentro da action, depois de carregar a membership do caller:
|
|
269
|
+
const callerMembership = await getTeamMembership(user.id, teamId);
|
|
270
|
+
if (callerMembership?.role !== 'owner') {
|
|
271
|
+
return { error: 'Apenas o dono do time pode gerenciar membros.' };
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
E toda ação sensível (convidar, remover, mudar plano) deve gravar em `activityLogs` via `logActivity`:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
async function logActivity(teamId, userId, type: ActivityType, ipAddress?) {
|
|
279
|
+
if (teamId == null) return;
|
|
280
|
+
await db.insert(activityLogs).values({
|
|
281
|
+
teamId, userId, action: type, ipAddress: ipAddress ?? '',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 6. O fluxo de convite (signup que aceita invite)
|
|
287
|
+
|
|
288
|
+
Convite é por e-mail com `status: 'pending'`. No signup, se vier `inviteId`, a action procura o convite
|
|
289
|
+
**casando id + email + status pending**, e só então herda a role do convite e marca `accepted`:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const [invitation] = await db.select().from(invitations).where(and(
|
|
293
|
+
eq(invitations.id, parseInt(inviteId)),
|
|
294
|
+
eq(invitations.email, email),
|
|
295
|
+
eq(invitations.status, 'pending'),
|
|
296
|
+
)).limit(1);
|
|
297
|
+
|
|
298
|
+
if (invitation) {
|
|
299
|
+
userRole = invitation.role; // herda role do convite
|
|
300
|
+
await db.update(invitations).set({ status: 'accepted' })
|
|
301
|
+
.where(eq(invitations.id, invitation.id));
|
|
302
|
+
} else {
|
|
303
|
+
// sem convite válido -> cria team novo e vira 'owner'
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
Sem convite válido, o usuário vira `owner` de um team novo. O casamento por e-mail impede aceitar convite
|
|
307
|
+
de outra pessoa.
|
|
308
|
+
|
|
309
|
+
## Checklist
|
|
310
|
+
|
|
311
|
+
- [ ] Os campos `stripeCustomerId / stripeSubscriptionId / planName / subscriptionStatus` estão em
|
|
312
|
+
**`teams`**, não em `users`?
|
|
313
|
+
- [ ] O acesso premium é decidido lendo `teams.subscriptionStatus` (escrito pelo webhook) e **não** pelo
|
|
314
|
+
redirect de sucesso do Checkout?
|
|
315
|
+
- [ ] O webhook verifica a assinatura com `stripe.webhooks.constructEvent` usando o **raw body** e
|
|
316
|
+
`STRIPE_WEBHOOK_SECRET`, retornando 400 se falhar?
|
|
317
|
+
- [ ] O webhook trata `customer.subscription.updated` **e** `customer.subscription.deleted`, e
|
|
318
|
+
**zera** os campos no `canceled`/`unpaid`?
|
|
319
|
+
- [ ] Trocar cartão / cancelar / upgrade vão pro **Customer Portal** hospedado (zero tela de cartão no
|
|
320
|
+
seu código)?
|
|
321
|
+
- [ ] O cookie de sessão é `httpOnly: true`, `secure: true`, `sameSite: 'lax'`, e o JWT é `HS256` com
|
|
322
|
+
`AUTH_SECRET`?
|
|
323
|
+
- [ ] O middleware renova o token nos GETs (sessão deslizante) e usa `runtime: 'nodejs'`?
|
|
324
|
+
- [ ] A autorização **fina** mora na Server Action (não no middleware), via wrappers
|
|
325
|
+
`validatedActionWithUser` / `withTeam`?
|
|
326
|
+
- [ ] Convidar / remover / mudar plano checam `teamMembers.role === 'owner'` **antes** de mutar?
|
|
327
|
+
(o starter NÃO faz — é o buraco a fechar)
|
|
328
|
+
- [ ] Toda ação sensível grava `activityLogs` com `ActivityType` e IP?
|
|
329
|
+
- [ ] Convite casa **id + email + status pending** antes de herdar a role?
|
|
330
|
+
- [ ] Em dev, o webhook chega via `stripe listen --forward-to .../api/stripe/webhook` com `whsec_`?
|
|
331
|
+
|
|
332
|
+
## Tabela de decisão
|
|
333
|
+
|
|
334
|
+
| Você precisa de... | Faça assim (grounded no starter) | Não faça |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| Unidade de cobrança | Campos Stripe em `teams`; 1 `stripeCustomerId` por team | Billing por usuário individual |
|
|
337
|
+
| Liberar acesso premium | Ler `teams.subscriptionStatus` (sync do webhook) | Confiar no `success_url` do Checkout |
|
|
338
|
+
| Coletar pagamento | Stripe Checkout hospedado (`mode: 'subscription'`) | Formulário de cartão próprio (vira escopo PCI) |
|
|
339
|
+
| Trocar cartão / cancelar / upgrade | Stripe Customer Portal hospedado | Construir telas de billing do zero |
|
|
340
|
+
| Saber o estado real da assinatura | Webhook com `constructEvent` + `STRIPE_WEBHOOK_SECRET` | Polling da API ou estado no cliente |
|
|
341
|
+
| Trial | `subscription_data: { trial_period_days: 14 }` | Lógica de trial própria no seu banco |
|
|
342
|
+
| Sessão | JWT `HS256` (jose) em cookie `httpOnly`+`secure`+`sameSite:lax` | Token em `localStorage` (XSS lê) |
|
|
343
|
+
| Gate de rota | Middleware: tem cookie? rota protegida? renova no GET | Checar permissão fina no middleware |
|
|
344
|
+
| Permissão fina (quem pode o quê) | Na Server Action, cheque `teamMembers.role === 'owner'` | Assumir que membership == permissão |
|
|
345
|
+
| Validar input de action | `validatedAction*` + `zod.safeParse(Object.fromEntries(formData))` | Validação manual espalhada |
|
|
346
|
+
| Auditoria | `logActivity` append-only em `activityLogs` com `ActivityType` | Sem trilha (impossível investigar abuso) |
|
|
347
|
+
| Convite de membro | `invitations` por e-mail, `pending` → `accepted` casando id+email | Adicionar membro direto sem aceite |
|
|
348
|
+
| Runtime do middleware | `runtime: 'nodejs'` (jose não roda em Edge) | Edge runtime com `jose`/crypto Node |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
**Fonte:** [`nextjs/saas-starter`](https://github.com/nextjs/saas-starter) (Vercel) — Next.js App Router,
|
|
353
|
+
Postgres, Drizzle, Stripe, shadcn/ui. Schema, fluxo de billing, auth, middleware e actions extraídos do
|
|
354
|
+
código real do template. O buraco de RBAC em `removeTeamMember`/`inviteTeamMember` é uma característica
|
|
355
|
+
conhecida do starter — está aqui de propósito, como armadilha a corrigir.
|