@luanpdd/kit-mcp 1.28.0 → 1.30.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.
Files changed (332) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/gates/agent-no-recursive-dispatch.md +82 -82
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/README.md +76 -76
  6. package/kit/agents/advisor-researcher.md +106 -106
  7. package/kit/agents/assumptions-analyzer.md +107 -107
  8. package/kit/agents/audit-log-implementer.md +313 -313
  9. package/kit/agents/auditor-consistencia-isolamento.md +413 -413
  10. package/kit/agents/b2b-saas-architect.md +156 -156
  11. package/kit/agents/cascading-failures-auditor.md +298 -298
  12. package/kit/agents/codebase-mapper.md +768 -768
  13. package/kit/agents/crm-pipeline-implementer.md +256 -256
  14. package/kit/agents/debugger.md +813 -813
  15. package/kit/agents/detector-tenant-quente.md +337 -337
  16. package/kit/agents/evolution-go-integrator.md +200 -200
  17. package/kit/agents/example-reviewer.md +21 -21
  18. package/kit/agents/executor.md +564 -564
  19. package/kit/agents/integration-checker.md +200 -200
  20. package/kit/agents/invite-flow-implementer.md +189 -189
  21. package/kit/agents/legacy-characterizer.md +368 -368
  22. package/kit/agents/lgpd-compliance-auditor.md +295 -295
  23. package/kit/agents/multi-tenant-isolation-auditor.md +253 -253
  24. package/kit/agents/multi-tenant-rls-writer.md +340 -340
  25. package/kit/agents/nyquist-auditor.md +178 -178
  26. package/kit/agents/observability-coverage-auditor.md +315 -315
  27. package/kit/agents/org-onboarding-implementer.md +223 -223
  28. package/kit/agents/payload-capture-instrumenter.md +273 -273
  29. package/kit/agents/phase-researcher.md +696 -696
  30. package/kit/agents/plan-checker.md +272 -272
  31. package/kit/agents/planner.md +922 -922
  32. package/kit/agents/project-researcher.md +652 -652
  33. package/kit/agents/refactor-safety-auditor.md +404 -404
  34. package/kit/agents/research-synthesizer.md +245 -245
  35. package/kit/agents/roadmapper.md +677 -677
  36. package/kit/agents/seam-finder.md +359 -359
  37. package/kit/agents/shotgun-surgery-detector.md +349 -349
  38. package/kit/agents/supabase-branching-architect.md +562 -562
  39. package/kit/agents/supabase-cicd-pipeline-implementer.md +777 -777
  40. package/kit/agents/supabase-column-privileges-writer.md +399 -399
  41. package/kit/agents/supabase-edge-fn-tester.md +287 -0
  42. package/kit/agents/supabase-edge-fn-writer.md +239 -210
  43. package/kit/agents/supabase-migration-writer.md +385 -385
  44. package/kit/agents/supabase-rbac-implementer.md +392 -392
  45. package/kit/agents/supabase-realtime-implementer.md +363 -267
  46. package/kit/agents/supabase-rls-hardener.md +521 -521
  47. package/kit/agents/supabase-rls-writer.md +323 -323
  48. package/kit/agents/supabase-roles-implementer.md +355 -355
  49. package/kit/agents/super-admin-implementer.md +281 -281
  50. package/kit/agents/ui-auditor.md +437 -437
  51. package/kit/agents/ui-checker.md +302 -302
  52. package/kit/agents/ui-researcher.md +355 -355
  53. package/kit/agents/user-profiler.md +175 -175
  54. package/kit/agents/validador-evolucao-schema.md +335 -335
  55. package/kit/agents/verifier.md +728 -728
  56. package/kit/commands/adicionar-backlog.md +75 -75
  57. package/kit/commands/adicionar-fase.md +42 -42
  58. package/kit/commands/adicionar-tarefa.md +45 -45
  59. package/kit/commands/adicionar-testes.md +41 -41
  60. package/kit/commands/ajuda.md +21 -21
  61. package/kit/commands/atualizar.md +37 -37
  62. package/kit/commands/auditar-cascading.md +111 -111
  63. package/kit/commands/auditar-marco.md +179 -179
  64. package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
  65. package/kit/commands/auditar-refactor.md +219 -219
  66. package/kit/commands/auditar-release.md +109 -109
  67. package/kit/commands/auditar-uat.md +23 -23
  68. package/kit/commands/autonomo.md +40 -40
  69. package/kit/commands/branch-pr.md +24 -24
  70. package/kit/commands/burn-rate-status.md +408 -408
  71. package/kit/commands/capturar-payloads.md +193 -193
  72. package/kit/commands/caracterizar.md +212 -212
  73. package/kit/commands/concluir-marco.md +247 -247
  74. package/kit/commands/configuracoes.md +36 -36
  75. package/kit/commands/dados-distribuidos.md +188 -188
  76. package/kit/commands/definir-perfil.md +10 -10
  77. package/kit/commands/depurar.md +190 -190
  78. package/kit/commands/detectar-duplicacao.md +197 -197
  79. package/kit/commands/discutir-fase.md +131 -131
  80. package/kit/commands/encontrar-seams.md +136 -136
  81. package/kit/commands/entrar-discord.md +17 -17
  82. package/kit/commands/estatisticas.md +18 -18
  83. package/kit/commands/example-greeting.md +33 -33
  84. package/kit/commands/executar-fase.md +58 -58
  85. package/kit/commands/expresso.md +56 -56
  86. package/kit/commands/fase-ui.md +34 -34
  87. package/kit/commands/fazer.md +57 -57
  88. package/kit/commands/fio.md +125 -125
  89. package/kit/commands/fluxos-trabalho.md +64 -64
  90. package/kit/commands/forense.md +176 -176
  91. package/kit/commands/gerenciador.md +38 -38
  92. package/kit/commands/inserir-fase.md +31 -31
  93. package/kit/commands/legacy.md +263 -263
  94. package/kit/commands/limpeza.md +17 -17
  95. package/kit/commands/listar-hipoteses-fase.md +45 -45
  96. package/kit/commands/listar-workspaces.md +18 -18
  97. package/kit/commands/load-shedding.md +117 -117
  98. package/kit/commands/mapear-codebase.md +70 -70
  99. package/kit/commands/multi-tenant.md +163 -163
  100. package/kit/commands/nota.md +33 -33
  101. package/kit/commands/novo-marco.md +43 -43
  102. package/kit/commands/novo-projeto.md +41 -41
  103. package/kit/commands/novo-workspace.md +43 -43
  104. package/kit/commands/pausar-trabalho.md +37 -37
  105. package/kit/commands/perfil-usuario.md +45 -45
  106. package/kit/commands/pesquisar-fase.md +195 -195
  107. package/kit/commands/planejar-fase.md +67 -67
  108. package/kit/commands/planejar-lacunas.md +33 -33
  109. package/kit/commands/plantar-ideia.md +25 -25
  110. package/kit/commands/progresso.md +24 -24
  111. package/kit/commands/proximo.md +30 -30
  112. package/kit/commands/publicar.md +490 -490
  113. package/kit/commands/rapido.md +35 -35
  114. package/kit/commands/reaplicar-patches.md +124 -124
  115. package/kit/commands/refactor-seguro.md +321 -321
  116. package/kit/commands/relatorio-sessao.md +19 -19
  117. package/kit/commands/remover-fase.md +31 -31
  118. package/kit/commands/remover-workspace.md +26 -26
  119. package/kit/commands/resumo-marco.md +50 -50
  120. package/kit/commands/retomar-trabalho.md +40 -40
  121. package/kit/commands/revisar-backlog.md +60 -60
  122. package/kit/commands/revisar-ui.md +32 -32
  123. package/kit/commands/revisar.md +37 -37
  124. package/kit/commands/saude.md +21 -21
  125. package/kit/commands/setup-notion.md +93 -93
  126. package/kit/commands/storytelling.md +179 -179
  127. package/kit/commands/supabase.md +30 -7
  128. package/kit/commands/sync-main.md +68 -68
  129. package/kit/commands/validar-fase.md +35 -35
  130. package/kit/commands/verificar-tarefas.md +44 -44
  131. package/kit/commands/verificar-trabalho.md +64 -64
  132. package/kit/file-manifest.json +14 -8
  133. package/kit/framework/bin/lib/commands.cjs +959 -959
  134. package/kit/framework/bin/lib/config.cjs +442 -442
  135. package/kit/framework/bin/lib/core.cjs +1230 -1230
  136. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  137. package/kit/framework/bin/lib/init.cjs +1442 -1442
  138. package/kit/framework/bin/lib/milestone.cjs +252 -252
  139. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  140. package/kit/framework/bin/lib/phase.cjs +888 -888
  141. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  142. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  143. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  144. package/kit/framework/bin/lib/security.cjs +382 -382
  145. package/kit/framework/bin/lib/state.cjs +1031 -1031
  146. package/kit/framework/bin/lib/template.cjs +222 -222
  147. package/kit/framework/bin/lib/uat.cjs +282 -282
  148. package/kit/framework/bin/lib/verify.cjs +888 -888
  149. package/kit/framework/bin/lib/workstream.cjs +491 -491
  150. package/kit/framework/bin/tools.cjs +918 -918
  151. package/kit/framework/commands/workstreams.md +63 -63
  152. package/kit/framework/references/checkpoints.md +778 -778
  153. package/kit/framework/references/continuation-format.md +249 -249
  154. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  155. package/kit/framework/references/git-integration.md +295 -295
  156. package/kit/framework/references/git-planning-commit.md +38 -38
  157. package/kit/framework/references/model-profile-resolution.md +36 -36
  158. package/kit/framework/references/model-profiles.md +139 -139
  159. package/kit/framework/references/phase-argument-parsing.md +61 -61
  160. package/kit/framework/references/planning-config.md +202 -202
  161. package/kit/framework/references/questioning.md +162 -162
  162. package/kit/framework/references/tdd.md +263 -263
  163. package/kit/framework/references/ui-brand.md +160 -160
  164. package/kit/framework/references/user-profiling.md +657 -657
  165. package/kit/framework/references/verification-patterns.md +612 -612
  166. package/kit/framework/references/workstream-flag.md +58 -58
  167. package/kit/framework/templates/DEBUG.md +164 -164
  168. package/kit/framework/templates/UAT.md +265 -265
  169. package/kit/framework/templates/UI-SPEC.md +100 -100
  170. package/kit/framework/templates/VALIDATION.md +76 -76
  171. package/kit/framework/templates/claude-md.md +122 -122
  172. package/kit/framework/templates/codebase/architecture.md +185 -185
  173. package/kit/framework/templates/codebase/concerns.md +205 -205
  174. package/kit/framework/templates/codebase/conventions.md +204 -204
  175. package/kit/framework/templates/codebase/integrations.md +192 -192
  176. package/kit/framework/templates/codebase/stack.md +158 -158
  177. package/kit/framework/templates/codebase/structure.md +199 -199
  178. package/kit/framework/templates/codebase/testing.md +301 -301
  179. package/kit/framework/templates/config.json +44 -44
  180. package/kit/framework/templates/context.md +352 -352
  181. package/kit/framework/templates/continue-here.md +78 -78
  182. package/kit/framework/templates/copilot-instructions.md +7 -7
  183. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  184. package/kit/framework/templates/dev-preferences.md +20 -20
  185. package/kit/framework/templates/discovery.md +146 -146
  186. package/kit/framework/templates/discussion-log.md +63 -63
  187. package/kit/framework/templates/milestone-archive.md +123 -123
  188. package/kit/framework/templates/milestone.md +115 -115
  189. package/kit/framework/templates/phase-prompt.md +610 -610
  190. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  191. package/kit/framework/templates/project.md +186 -186
  192. package/kit/framework/templates/requirements.md +231 -231
  193. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  194. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  195. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  196. package/kit/framework/templates/research-project/STACK.md +120 -120
  197. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  198. package/kit/framework/templates/research.md +419 -419
  199. package/kit/framework/templates/retrospective.md +54 -54
  200. package/kit/framework/templates/roadmap.md +202 -202
  201. package/kit/framework/templates/state.md +176 -176
  202. package/kit/framework/templates/summary-complex.md +59 -59
  203. package/kit/framework/templates/summary-minimal.md +41 -41
  204. package/kit/framework/templates/summary-standard.md +48 -48
  205. package/kit/framework/templates/summary.md +209 -209
  206. package/kit/framework/templates/user-profile.md +146 -146
  207. package/kit/framework/templates/user-setup.md +256 -256
  208. package/kit/framework/templates/verification-report.md +258 -258
  209. package/kit/framework/workflows/add-phase.md +112 -112
  210. package/kit/framework/workflows/add-tests.md +351 -351
  211. package/kit/framework/workflows/add-todo.md +158 -158
  212. package/kit/framework/workflows/audit-milestone.md +340 -340
  213. package/kit/framework/workflows/audit-uat.md +109 -109
  214. package/kit/framework/workflows/autonomous.md +891 -891
  215. package/kit/framework/workflows/check-todos.md +177 -177
  216. package/kit/framework/workflows/cleanup.md +152 -152
  217. package/kit/framework/workflows/complete-milestone.md +696 -696
  218. package/kit/framework/workflows/diagnose-issues.md +231 -231
  219. package/kit/framework/workflows/discovery-phase.md +289 -289
  220. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  221. package/kit/framework/workflows/discuss-phase.md +784 -784
  222. package/kit/framework/workflows/do.md +104 -104
  223. package/kit/framework/workflows/execute-phase.md +838 -838
  224. package/kit/framework/workflows/execute-plan.md +510 -510
  225. package/kit/framework/workflows/fast.md +102 -102
  226. package/kit/framework/workflows/forensics.md +265 -265
  227. package/kit/framework/workflows/health.md +181 -181
  228. package/kit/framework/workflows/help.md +619 -619
  229. package/kit/framework/workflows/insert-phase.md +130 -130
  230. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  231. package/kit/framework/workflows/list-workspaces.md +56 -56
  232. package/kit/framework/workflows/manager.md +362 -362
  233. package/kit/framework/workflows/map-codebase.md +377 -377
  234. package/kit/framework/workflows/milestone-summary.md +223 -223
  235. package/kit/framework/workflows/new-milestone.md +486 -486
  236. package/kit/framework/workflows/new-project.md +1159 -1159
  237. package/kit/framework/workflows/new-workspace.md +237 -237
  238. package/kit/framework/workflows/next.md +97 -97
  239. package/kit/framework/workflows/node-repair.md +92 -92
  240. package/kit/framework/workflows/note.md +156 -156
  241. package/kit/framework/workflows/pause-work.md +176 -176
  242. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  243. package/kit/framework/workflows/plan-phase.md +765 -765
  244. package/kit/framework/workflows/plant-seed.md +169 -169
  245. package/kit/framework/workflows/pr-branch.md +129 -129
  246. package/kit/framework/workflows/profile-user.md +450 -450
  247. package/kit/framework/workflows/progress.md +507 -507
  248. package/kit/framework/workflows/quick.md +757 -757
  249. package/kit/framework/workflows/remove-phase.md +155 -155
  250. package/kit/framework/workflows/remove-workspace.md +90 -90
  251. package/kit/framework/workflows/research-phase.md +82 -82
  252. package/kit/framework/workflows/resume-project.md +326 -326
  253. package/kit/framework/workflows/review.md +228 -228
  254. package/kit/framework/workflows/session-report.md +146 -146
  255. package/kit/framework/workflows/settings.md +283 -283
  256. package/kit/framework/workflows/ship.md +228 -228
  257. package/kit/framework/workflows/stats.md +60 -60
  258. package/kit/framework/workflows/transition.md +671 -671
  259. package/kit/framework/workflows/ui-phase.md +302 -302
  260. package/kit/framework/workflows/ui-review.md +165 -165
  261. package/kit/framework/workflows/update.md +323 -323
  262. package/kit/framework/workflows/validate-phase.md +174 -174
  263. package/kit/framework/workflows/verify-phase.md +252 -252
  264. package/kit/framework/workflows/verify-work.md +637 -637
  265. package/kit/hooks/check-update.js +118 -118
  266. package/kit/hooks/context-monitor.js +163 -163
  267. package/kit/hooks/prompt-guard.js +103 -103
  268. package/kit/hooks/statusline.js +125 -125
  269. package/kit/hooks/workflow-guard.js +101 -101
  270. package/kit/settings.json +45 -45
  271. package/kit/skills/_shared-supabase/glossary.md +17 -0
  272. package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
  273. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
  274. package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
  275. package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
  276. package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
  277. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
  278. package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
  279. package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
  280. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
  281. package/kit/skills/example-skill/SKILL.md +42 -42
  282. package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
  283. package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
  284. package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
  285. package/kit/skills/legacy-extract-class/SKILL.md +203 -203
  286. package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
  287. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
  288. package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
  289. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
  290. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
  291. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
  292. package/kit/skills/member-invite-flow/SKILL.md +305 -305
  293. package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
  294. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
  295. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
  296. package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
  297. package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
  298. package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
  299. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
  300. package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
  301. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
  302. package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
  303. package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
  304. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
  305. package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
  306. package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
  307. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
  308. package/kit/skills/supabase-edge-functions/SKILL.md +229 -141
  309. package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -0
  310. package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -0
  311. package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -0
  312. package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -0
  313. package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -0
  314. package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
  315. package/kit/skills/supabase-migrations/SKILL.md +297 -297
  316. package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
  317. package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
  318. package/kit/skills/supabase-realtime/SKILL.md +460 -236
  319. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
  320. package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
  321. package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
  322. package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
  323. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
  324. package/package.json +1 -1
  325. package/src/cli/index.js +33 -0
  326. package/src/core/kit.js +216 -216
  327. package/src/core/reflect.js +247 -247
  328. package/src/core/reverse-sync.js +372 -372
  329. package/src/core/sync.js +418 -418
  330. package/src/core/watch.js +121 -121
  331. package/src/mcp-server/index.js +693 -490
  332. package/src/mcp-server/roots.js +124 -0
@@ -1,712 +1,712 @@
1
- ---
2
- name: streams-eventos-cdc
3
- description: Use ao implementar event stream em Supabase — diferença AMQP/JMS-style (LISTEN/NOTIFY) vs log-based (pgmq) brokers, padrões CDC via wal2json + Realtime broadcast OU pglogical → Kafka, ev…
4
- ---
5
-
6
- # Streams, Eventos e CDC — Brokers, Event Sourcing, Exactly-Once em Postgres
7
-
8
- ## Quando usar
9
-
10
- LLM carrega esta skill ao implementar pipeline event-driven em Supabase + Postgres. Trigger phrases:
11
-
12
- - "event stream Postgres", "CDC Supabase", "wal2json + Realtime"
13
- - "pgmq vs LISTEN/NOTIFY", "broker log-based vs AMQP"
14
- - "event sourcing Postgres", "tabela append-only de eventos"
15
- - "exactly-once pgmq", "dedup table idempotency"
16
- - "stream join com janela", "stream-table CDC enrichment"
17
- - "log compaction Postgres", "snapshot eventos"
18
- - "projeção materialized view de eventos", "denormalization via trigger"
19
-
20
- Esta skill **estende** [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) ao reconhecer audit_log como event sourcing parcial; [`supabase-cron-queues`](../supabase-cron-queues/SKILL.md) (v1.8) para pgmq pattern; e [`supabase-realtime`](../supabase-realtime/SKILL.md) (v1.8) para broadcast como CDC stream.
21
-
22
- Material-fonte: *Designing Data-Intensive Applications*, Martin Kleppmann (O'Reilly 2017), capítulo 11 "Stream Processing" (linhas 17812-19637 do material extraído; summary 19408-19481). Termos canônicos PT-BR ↔ EN definidos em [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) seção (h).
23
-
24
- ## Regras absolutas
25
-
26
- **REGRA #1 (broker log-based default para event sourcing):** Para event sourcing, CDC ou pipeline com replay obrigatório, escolher **log-based broker** (Kafka, pgmq) — mensagem retida (TTL configurável), múltiplos consumers tracked offset independente, replay possível. AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY) deletam mensagem após ack — sem replay, single-consumer.
27
-
28
- **REGRA #2 (CDC via wal2json + Realtime é default Supabase):** Se ambiente é Supabase + use case é sync índice/desnormalização/multi-region, default é wal2json + Supabase Realtime broadcast. Zero infra extra. Apenas considerar pglogical → Kafka externo se warehousing analítico for o uso primário.
29
-
30
- **REGRA #3 (event sourcing exige tabela append-only + projeções derivadas):** Tabela `events` deve ser **append-only** (REVOKE DELETE/UPDATE como audit_log v1.21). Estado atual = projeção derivada via Materialized View ou trigger-maintained denormalization — NUNCA escrever direto em "tabela de estado". Source of truth = stream de eventos.
31
-
32
- **REGRA #4 (exactly-once pgmq exige dedup + idempotency + transactional outbox):** pgmq não garante exactly-once nativo (at-least-once entrega). Para semântica exactly-once: (a) **dedup table** com `unique(event_id)` rejeitando duplicatas; (b) **handler idempotente** (mesmo input → mesmo output, sem efeitos colaterais); (c) **transactional outbox** para cross-service writes.
33
-
34
- **REGRA #5 (stream join exige janela temporal explícita):** Stream-stream join sem janela = memória cresce sem limite (cada evento aguarda match indefinidamente). Toda janela deve ter TTL explícito (tumbling, sliding, session). Default: tumbling 5min para business events; sliding 1min para latency-sensitive.
35
-
36
- **REGRA #6 (log compaction não-trivial em pgmq — exige snapshot manual):** pgmq não tem log compaction nativa (Kafka tem). Para event sourcing com snapshot: criar tabela `snapshots` periodicamente, deletar `events.id < snapshot_lsn` correspondente. Sem snapshot = tabela `events` cresce sem limite, replay torna-se O(n) caro.
37
-
38
- ## Patterns canônicos
39
-
40
- ### REQ STREAMS-01 — Brokers AMQP/JMS-style vs log-based
41
-
42
- | Tipo | Exemplos | Mensagem após ack | Multi-consumer | Replay | Use case |
43
- |---|---|---|---|---|---|
44
- | **AMQP/JMS-style** | RabbitMQ, postgres `LISTEN/NOTIFY`, ActiveMQ | Deletada (consumida) | Single (work queue — distribui rounds robin) | Não (gone after ack) | Task queue async (envio email, geração PDF) |
45
- | **Log-based** | Kafka, pgmq, Redpanda, Pulsar | Retida (TTL configurável) | Multiple (cada consumer tracks offset independente) | Sim (replay desde offset N) | Event sourcing, CDC, audit, analytics |
46
-
47
- **Como escolher:**
48
-
49
- ```
50
- Use case precisa de replay? ─── Sim ──► log-based (pgmq, Kafka)
51
-
52
- Não
53
-
54
-
55
- Múltiplos consumers veem mesma mensagem? ─── Sim ──► log-based
56
-
57
- Não
58
-
59
-
60
- Mensagem é "task" descartável após processada? ─── Sim ──► AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY)
61
-
62
- Não
63
-
64
-
65
- Default (event-driven em B2B SaaS): log-based (pgmq)
66
- ```
67
-
68
- **Exemplo postgres LISTEN/NOTIFY (AMQP-style — single consumer, sem replay):**
69
-
70
- ```sql
71
- -- Producer
72
- notify ch_orders, '{"order_id":"abc-123","status":"paid"}';
73
-
74
- -- Consumer (Edge Function)
75
- listen ch_orders;
76
- -- Sleep até receber notification — single consumer recebe, mensagem some
77
- ```
78
-
79
- **Exemplo pgmq (log-based — multi-consumer, replay):**
80
-
81
- ```sql
82
- -- Setup (uma vez)
83
- select pgmq.create('orders');
84
-
85
- -- Producer
86
- select pgmq.send('orders', '{"order_id":"abc-123","status":"paid"}');
87
-
88
- -- Consumer 1 (worker A)
89
- select * from pgmq.read('orders', 30, 1);
90
- -- vt=30s (visibility timeout), 1 mensagem por leitura
91
- -- Após ler: mensagem fica invisível por 30s — outro worker não pega
92
- -- Worker A processa e dá ack:
93
- select pgmq.delete('orders', msg_id);
94
- -- Sem ack em 30s → mensagem volta à queue (at-least-once)
95
-
96
- -- Consumer 2 (worker B / archive)
97
- -- Se queue tem retention, archive table mantém histórico para replay
98
- select * from pgmq.archive('orders', msg_id);
99
- -- Mensagens em archive são replayable
100
- ```
101
-
102
- ### REQ STREAMS-02 — 3 padrões CDC em Postgres
103
-
104
- CDC (Change Data Capture) = capturar mudanças no DB como stream de eventos. 3 abordagens canônicas em Supabase:
105
-
106
- **Abordagem 1: wal2json + Supabase Realtime broadcast** (default)
107
-
108
- ```sql
109
- -- Habilitar replication identity (necessário para wal2json capturar UPDATE/DELETE com colunas)
110
- alter table public.orders replica identity full;
111
-
112
- -- Supabase Realtime já consome WAL via wal2json internamente
113
- -- Cliente subscreve canal específico via JS client
114
- ```
115
-
116
- ```typescript
117
- // Cliente Supabase consume CDC stream via Realtime
118
- const channel = supabase
119
- .channel('orders-cdc', { config: { private: true } })
120
- .on(
121
- 'postgres_changes',
122
- { event: '*', schema: 'public', table: 'orders' },
123
- (payload) => {
124
- // payload.eventType: INSERT | UPDATE | DELETE
125
- // payload.new: nova row (INSERT/UPDATE)
126
- // payload.old: row antiga (UPDATE/DELETE — exige replica identity full)
127
- console.log('CDC event:', payload);
128
- }
129
- )
130
- .subscribe();
131
- ```
132
-
133
- **Trade-offs:** zero infra extra; baixa latência (sub-segundo); RLS aplicada nas mensagens (cada cliente vê só rows permitidas). Limite: scale na ordem de milhares de subscribers por canal.
134
-
135
- **Abordagem 2: pglogical → Kafka externo** (warehousing analítico)
136
-
137
- ```sql
138
- -- Em Supabase Pro+ habilitar pglogical (extensão)
139
- create extension if not exists pglogical;
140
-
141
- -- Setup nó provider (Postgres source)
142
- select pglogical.create_node(
143
- node_name := 'supabase_prod',
144
- dsn := 'host=db.xxx.supabase.co dbname=postgres'
145
- );
146
-
147
- -- Replication set para tabelas que viram stream
148
- select pglogical.create_replication_set(set_name := 'cdc_set');
149
- select pglogical.replication_set_add_table('cdc_set', 'public.orders', synchronize_data := false);
150
-
151
- -- Conector Kafka (Debezium ou similar) consome pglogical → publica em Kafka topic
152
- -- Trade-off: requer infra Kafka externa, latência maior (segundos), throughput muito maior
153
- ```
154
-
155
- **Abordagem 3: Trigger-based** (casos custom onde wal2json não cobre)
156
-
157
- ```sql
158
- -- Trigger que emite evento custom quando flag X muda
159
- create or replace function public.emit_lead_qualified_event()
160
- returns trigger
161
- language plpgsql
162
- security invoker
163
- set search_path = ''
164
- as $$
165
- begin
166
- if old.stage != 'qualified' and new.stage = 'qualified' then
167
- insert into public.outbox (event_type, payload)
168
- values (
169
- 'lead_qualified',
170
- jsonb_build_object(
171
- 'lead_id', new.id,
172
- 'org_id', new.org_id,
173
- 'qualified_by', (select auth.uid()),
174
- 'qualified_at', now()
175
- )
176
- );
177
- end if;
178
- return new;
179
- end;
180
- $$;
181
-
182
- create trigger lead_qualified_trigger
183
- after update on public.leads
184
- for each row
185
- execute function public.emit_lead_qualified_event();
186
- ```
187
-
188
- **Quando usar trigger-based:** semântica de evento mais rica que "linha mudou" (ex: business event "qualified" derivado de mudança específica). Worker async lê outbox e publica downstream.
189
-
190
- **Use cases canônicos:**
191
-
192
- | Use case | Abordagem recomendada |
193
- |---|---|
194
- | Sync índice de busca (Elasticsearch, Meilisearch) | wal2json + Realtime → função client que sincroniza |
195
- | Desnormalização (Materialized View atualizada por evento) | Trigger-based (mais controle sobre quando refresh) |
196
- | Sync multi-region cold standby | pglogical → Kafka → consumer remoto |
197
- | Audit log retroativo + análise comportamental | wal2json (captura cru) → analytics warehouse |
198
- | Notificação push (mobile app) | Realtime broadcast direto (zero step intermediário) |
199
-
200
- ### REQ STREAMS-03 — Event sourcing em Postgres
201
-
202
- **Princípio canônico:** eventos imutáveis são source of truth; estado atual é projeção derivada.
203
-
204
- **Schema canônico:**
205
-
206
- ```sql
207
- -- Tabela events — source of truth (append-only)
208
- create table public.events (
209
- id bigserial primary key,
210
- aggregate_id uuid not null, -- ID da entidade (order, user, ...)
211
- aggregate_type text not null, -- Tipo da entidade ('order', 'user')
212
- event_type text not null, -- 'order_created', 'order_paid', 'order_shipped'
213
- payload jsonb not null, -- Detalhes do evento
214
- metadata jsonb, -- actor_id, request_id, trace_id
215
- created_at timestamptz not null default now()
216
- );
217
-
218
- -- Index canônico (para reproduzir histórico de uma entidade)
219
- create index events_aggregate_idx on public.events (aggregate_id, id);
220
-
221
- -- Index para query por tipo (analytics)
222
- create index events_type_created_idx on public.events (event_type, created_at);
223
-
224
- -- REGRA #3 — append-only: REVOKE DELETE/UPDATE
225
- revoke delete, update on public.events from public, authenticated, anon, service_role;
226
- -- Apenas postgres role pode deletar (cleanup com snapshot)
227
- ```
228
-
229
- **Cross-ref ATIVO** para [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) — audit_log É event sourcing semantics: append-only, imutável, retém histórico cronológico. Quem implementou audit_log já fez event sourcing parcial.
230
-
231
- **Projeção via Materialized View:**
232
-
233
- ```sql
234
- -- Projeção: estado atual de cada order derivado dos eventos
235
- create materialized view public.order_state as
236
- select
237
- aggregate_id as order_id,
238
- -- Reconstrói estado a partir dos eventos (último win)
239
- (array_agg(payload->>'status' order by id desc))[1] as current_status,
240
- (array_agg(payload->>'total' order by id desc))[1]::numeric as current_total,
241
- min(created_at) as created_at,
242
- max(created_at) as updated_at,
243
- count(*) as event_count
244
- from public.events
245
- where aggregate_type = 'order'
246
- group by aggregate_id;
247
-
248
- create unique index on public.order_state (order_id);
249
-
250
- -- Refresh (incremental via concurrent OR full)
251
- refresh materialized view concurrently public.order_state;
252
- -- Ou via pg_cron a cada N minutos
253
- ```
254
-
255
- **Projeção via trigger-maintained denormalization:**
256
-
257
- ```sql
258
- -- Tabela de estado mantida por trigger (atualizada por cada novo evento)
259
- create table public.order_current_state (
260
- order_id uuid primary key,
261
- status text not null,
262
- total numeric,
263
- updated_at timestamptz not null default now()
264
- );
265
-
266
- create or replace function public.project_order_event()
267
- returns trigger
268
- language plpgsql
269
- security invoker
270
- set search_path = ''
271
- as $$
272
- begin
273
- if new.aggregate_type = 'order' then
274
- insert into public.order_current_state (order_id, status, total, updated_at)
275
- values (
276
- new.aggregate_id,
277
- new.payload->>'status',
278
- (new.payload->>'total')::numeric,
279
- new.created_at
280
- )
281
- on conflict (order_id) do update
282
- set status = excluded.status,
283
- total = coalesce(excluded.total, public.order_current_state.total),
284
- updated_at = excluded.updated_at;
285
- end if;
286
- return new;
287
- end;
288
- $$;
289
-
290
- create trigger project_order_event_trigger
291
- after insert on public.events
292
- for each row
293
- execute function public.project_order_event();
294
- ```
295
-
296
- **Quando MV vs trigger:**
297
-
298
- | Critério | MV concurrent refresh | Trigger denormalization |
299
- |---|---|---|
300
- | **Latência** | Periódica (minutos) | Imediata (no commit do evento) |
301
- | **Custo write** | Baixo (write apenas em events) | Alto (write em events + state) |
302
- | **Custo read** | Baixo (state já agregado) | Baixo |
303
- | **Use case** | Analytics, dashboards | UI real-time, business state |
304
-
305
- ### REQ STREAMS-04 — Exactly-once em pgmq
306
-
307
- pgmq oferece **at-least-once** nativo (mensagem reenviada se worker crash sem ack). Para semântica **exactly-once**, combinação de 3 técnicas:
308
-
309
- **Técnica 1: Dedup table com unique(event_id)**
310
-
311
- ```sql
312
- -- Tabela de eventos já processados
313
- create table public.processed_events (
314
- event_id uuid primary key,
315
- processed_at timestamptz not null default now(),
316
- processor text not null -- nome do worker para debug
317
- );
318
- ```
319
-
320
- **Técnica 2: Handler atomic — INSERT na dedup + processamento na MESMA transação**
321
-
322
- ```sql
323
- -- Worker (Edge Function ou função PG)
324
- create or replace function public.process_order_event(p_msg_id bigint)
325
- returns void
326
- language plpgsql
327
- security definer -- worker tem privilégios elevados
328
- set search_path = ''
329
- as $$
330
- declare
331
- v_msg record;
332
- v_event_id uuid;
333
- begin
334
- -- Lê mensagem da queue com visibility timeout
335
- select msg_id, message into v_msg
336
- from pgmq.read('orders', 30, 1)
337
- limit 1;
338
-
339
- if v_msg is null then return; end if;
340
-
341
- v_event_id := (v_msg.message->>'event_id')::uuid;
342
-
343
- begin
344
- -- Atomic: INSERT dedup + processamento
345
- insert into public.processed_events (event_id, processor)
346
- values (v_event_id, 'process_order_event');
347
- -- Falha (unique violation) se já processado → exception abort tudo
348
-
349
- -- ... lógica de processamento idempotente ...
350
- update public.orders set status = 'paid' where id = (v_msg.message->>'order_id')::uuid;
351
-
352
- -- Ack — remove da queue
353
- perform pgmq.delete('orders', v_msg.msg_id);
354
-
355
- exception when unique_violation then
356
- -- Já processado — apenas dar ack para remover da queue
357
- perform pgmq.delete('orders', v_msg.msg_id);
358
- end;
359
- end;
360
- $$;
361
- ```
362
-
363
- **Técnica 3: Idempotency key no handler — mesmo input → mesmo output (sem efeitos colaterais)**
364
-
365
- Idempotency = processar a mesma mensagem N vezes produz o mesmo resultado. Padrões:
366
-
367
- ```sql
368
- -- Idempotente via UPDATE condicional (não muda se já está no estado)
369
- update public.orders
370
- set status = 'paid'
371
- where id = $1 and status != 'paid';
372
- -- Se já 'paid' → no-op, RETURNING vazio
373
-
374
- -- Idempotente via INSERT ON CONFLICT
375
- insert into public.payments (order_id, amount, transaction_id)
376
- values ($1, $2, $3)
377
- on conflict (transaction_id) do nothing;
378
- -- Mesmo transaction_id → no-op
379
- ```
380
-
381
- **Cross-ref ATIVO** para [`escolha-modelo-consistencia`](../escolha-modelo-consistencia/SKILL.md) — pattern transactional outbox descrito lá é a base de exactly-once entre DB e broker (write atomic em mesma transação).
382
-
383
- ### REQ STREAMS-05 — 3 tipos de stream join com SQL exemplo
384
-
385
- **Tipo 1: Stream-stream join (com janela temporal)**
386
-
387
- Match de eventos de 2 streams dentro de uma janela. Ex: matching pedido + pagamento dentro de 5min via tumbling window.
388
-
389
- ```sql
390
- -- Materialização: 2 tabelas event log + JOIN com window
391
- create table public.order_events (
392
- order_id uuid not null,
393
- event_at timestamptz not null,
394
- event_type text not null,
395
- payload jsonb
396
- );
397
-
398
- create table public.payment_events (
399
- payment_id uuid not null,
400
- order_id uuid not null,
401
- event_at timestamptz not null,
402
- amount numeric
403
- );
404
-
405
- -- Stream-stream join via tumbling window 5min
406
- create or replace view public.order_payment_join_5min as
407
- select
408
- o.order_id,
409
- o.event_at as order_at,
410
- p.event_at as paid_at,
411
- p.amount,
412
- date_trunc('minute', o.event_at) as window_start
413
- from public.order_events o
414
- join public.payment_events p on p.order_id = o.order_id
415
- where o.event_type = 'order_created'
416
- and p.event_at between o.event_at and o.event_at + interval '5 minutes'
417
- order by o.event_at;
418
- ```
419
-
420
- **Trade-off:** janela tumbling = não-overlapping, mais simples; sliding = overlapping, mais alertas; session = dinâmica, agrupada por user activity.
421
-
422
- **Tipo 2: Stream-table join (CDC + atividade — enrichment)**
423
-
424
- Stream de eventos enriquecido com lookup em tabela de referência atualizada por CDC.
425
-
426
- ```sql
427
- -- Tabela users mantida atualizada via CDC (Realtime ou trigger)
428
- -- Stream de eventos: clicks, logins, purchases — precisa enriched com user info
429
-
430
- select
431
- e.event_id,
432
- e.event_type,
433
- e.event_at,
434
- -- Enrichment: lookup do user no momento atual (não do momento do evento)
435
- u.email,
436
- u.tier,
437
- u.country
438
- from public.user_events e
439
- join public.users u on u.id = e.user_id
440
- where e.event_at > now() - interval '1 hour';
441
-
442
- -- Para latência baixa: keep tabela users em memória do worker (CDC stream → cache)
443
- ```
444
-
445
- **Cuidado canônico:** se a tabela mudou desde o evento, enrichment usa o estado **atual** do user, não o estado **no momento do evento**. Para histórico fiel: capturar snapshot no payload do evento (ex: `payload.user_email_at_event`).
446
-
447
- **Tipo 3: Table-table join (merge de changelogs CDC)**
448
-
449
- Merge de 2 changelogs CDC para produzir view denormalizada. Ex: orders changelog + customers changelog → view denormalizada de pedidos com info do cliente.
450
-
451
- ```sql
452
- -- Materialized view derivada de 2 streams CDC mergeados
453
- create materialized view public.orders_denorm as
454
- select
455
- o.order_id,
456
- o.status,
457
- o.total,
458
- o.created_at as order_created_at,
459
- c.email as customer_email,
460
- c.tier as customer_tier,
461
- c.country as customer_country
462
- from public.orders o
463
- join public.customers c on c.id = o.customer_id;
464
-
465
- create unique index on public.orders_denorm (order_id);
466
-
467
- -- Refresh disparado por CDC events em orders OU customers
468
- create or replace function public.refresh_orders_denorm()
469
- returns trigger
470
- language plpgsql
471
- as $$
472
- begin
473
- refresh materialized view concurrently public.orders_denorm;
474
- return null;
475
- end;
476
- $$;
477
-
478
- create trigger orders_changelog_trigger
479
- after insert or update on public.orders
480
- for each statement
481
- execute function public.refresh_orders_denorm();
482
-
483
- create trigger customers_changelog_trigger
484
- after update on public.customers
485
- for each statement
486
- execute function public.refresh_orders_denorm();
487
- ```
488
-
489
- **Trade-off:** refresh CONCURRENTLY exige unique index, latência maior. Para tabelas grandes, usar incremental refresh via trigger denormalization (REQ STREAMS-03).
490
-
491
- ### REQ STREAMS-06 — Log compaction strategy
492
-
493
- Log compaction = para cada chave, manter apenas o último valor. Reduz storage sem perder estado atual.
494
-
495
- **pgmq não tem nativa** — usa retention TTL via `vacuum_archive`:
496
-
497
- ```sql
498
- -- pgmq archive movido para tabela archive periodicamente
499
- select pgmq.archive('orders', 12345);
500
- -- Após N dias na archive, vacuum_archive deleta hard
501
-
502
- -- Configurar TTL via pg_cron
503
- select cron.schedule(
504
- 'pgmq_vacuum_archive',
505
- '0 2 * * *',
506
- $$ select pgmq.purge_archive('orders', 30); $$
507
- -- Deleta da archive mensagens > 30 dias
508
- );
509
- ```
510
-
511
- **Event sourcing exige snapshot periódico + compact:**
512
-
513
- ```sql
514
- -- Tabela de snapshots — estado materializado a cada N eventos
515
- create table public.snapshots (
516
- aggregate_id uuid primary key,
517
- snapshot_lsn bigint not null, -- até qual event.id este snapshot reflete
518
- state jsonb not null, -- estado serializado
519
- created_at timestamptz not null default now()
520
- );
521
-
522
- -- Função: criar snapshot para um aggregate quando event_count > threshold
523
- create or replace function public.create_snapshot(p_aggregate_id uuid)
524
- returns void
525
- language plpgsql
526
- security invoker
527
- set search_path = ''
528
- as $$
529
- declare
530
- v_state jsonb;
531
- v_snapshot_lsn bigint;
532
- begin
533
- -- Reproduzir todos os eventos para construir estado atual
534
- select
535
- jsonb_build_object(
536
- 'status', (array_agg(payload->>'status' order by id desc))[1],
537
- 'total', (array_agg(payload->>'total' order by id desc))[1]::numeric,
538
- 'event_count', count(*)
539
- ),
540
- max(id)
541
- into v_state, v_snapshot_lsn
542
- from public.events
543
- where aggregate_id = p_aggregate_id;
544
-
545
- -- Salvar snapshot (insert or update)
546
- insert into public.snapshots (aggregate_id, snapshot_lsn, state)
547
- values (p_aggregate_id, v_snapshot_lsn, v_state)
548
- on conflict (aggregate_id) do update
549
- set snapshot_lsn = excluded.snapshot_lsn,
550
- state = excluded.state,
551
- created_at = now();
552
- end;
553
- $$;
554
-
555
- -- Compact: deletar eventos < snapshot_lsn (tomados em consideração no snapshot)
556
- -- ATENÇÃO: requer privilégio especial (REGRA #3 — REVOKE DELETE em events)
557
- -- Apenas postgres role + função SECURITY DEFINER
558
- create or replace function public.compact_aggregate_events(p_aggregate_id uuid)
559
- returns int
560
- language plpgsql
561
- security definer
562
- set search_path = ''
563
- as $$
564
- declare
565
- v_deleted int;
566
- v_snapshot_lsn bigint;
567
- begin
568
- -- Confirmar que snapshot existe
569
- select snapshot_lsn into v_snapshot_lsn
570
- from public.snapshots
571
- where aggregate_id = p_aggregate_id;
572
-
573
- if v_snapshot_lsn is null then
574
- raise exception 'Snapshot ausente para aggregate_id %', p_aggregate_id;
575
- end if;
576
-
577
- -- Deletar eventos antes do snapshot
578
- delete from public.events
579
- where aggregate_id = p_aggregate_id
580
- and id <= v_snapshot_lsn;
581
-
582
- get diagnostics v_deleted = row_count;
583
- return v_deleted;
584
- end;
585
- $$;
586
-
587
- revoke execute on function public.compact_aggregate_events from public, authenticated, anon;
588
- -- Apenas service_role pode chamar
589
- ```
590
-
591
- **Estratégia canônica:** snapshot a cada 1000 eventos por aggregate; compact após snapshot validado (replay do snapshot reproduz estado atual). Sem snapshot/compact, replay para reconstruir estado torna-se O(n) caro em aggregates antigos.
592
-
593
- ## Anti-patterns
594
-
595
- ### Anti-pattern 1: Usar LISTEN/NOTIFY para event sourcing
596
-
597
- **Errado:**
598
-
599
- ```sql
600
- -- ❌ LISTEN/NOTIFY como "event log"
601
- notify ch_orders, '{"order_id":"abc","event":"paid"}';
602
- -- Consumer offline → mensagem perdida
603
- -- Sem replay, sem multi-consumer
604
- ```
605
-
606
- **Por quê:** LISTEN/NOTIFY é AMQP/JMS-style — single consumer ativo recebe, mensagem some. Se consumer offline durante notify, evento perdido. Sem replay.
607
-
608
- **Certo:** pgmq (log-based) ou tabela `events` append-only para event sourcing (REGRA #1).
609
-
610
- ### Anti-pattern 2: Event sourcing sem dedup → eventos duplicados
611
-
612
- **Errado:**
613
-
614
- ```sql
615
- -- ❌ Worker pgmq processa sem dedup table
616
- create or replace function public.process_event(p_msg jsonb)
617
- returns void
618
- language plpgsql
619
- as $$
620
- begin
621
- -- Processa direto, sem checar se já processado
622
- update public.orders set status = 'paid' where id = (p_msg->>'order_id')::uuid;
623
- -- Se mensagem reentregue (worker crash + redelivery) → status setado 2×
624
- -- Se webhook externo → cobra cliente 2×
625
- end;
626
- $$;
627
- ```
628
-
629
- **Por quê:** pgmq é at-least-once. Mensagem pode ser entregue >1× (worker crash sem ack, visibility timeout expirado). Sem dedup, processamento repetido = side effect duplicado.
630
-
631
- **Certo:** dedup table + handler idempotente (REGRA #4). Mesmo input → mesmo output.
632
-
633
- ### Anti-pattern 3: Stream-stream join sem janela temporal
634
-
635
- **Errado:**
636
-
637
- ```sql
638
- -- ❌ Sem janela temporal: memória cresce indefinidamente
639
- select o.order_id, p.payment_id
640
- from public.order_events o
641
- join public.payment_events p on p.order_id = o.order_id;
642
- -- Cada evento aguarda match indefinido — payment de 3 anos atrás casa com order recente
643
- -- Memória do worker cresce sem limite
644
- ```
645
-
646
- **Por quê:** stream join sem TTL = sistema mantém eventos em memória aguardando match. Memória cresce linearmente com tempo, eventualmente OOM.
647
-
648
- **Certo:** janela explícita (REGRA #5):
649
-
650
- ```sql
651
- -- ✅ Tumbling window 5min
652
- join public.payment_events p on p.order_id = o.order_id
653
- where p.event_at between o.event_at and o.event_at + interval '5 minutes';
654
- ```
655
-
656
- ### Anti-pattern 4: Materialized View sem CONCURRENTLY → bloqueio em refresh
657
-
658
- **Errado:**
659
-
660
- ```sql
661
- -- ❌ refresh sem CONCURRENTLY trava reads na MV durante refresh
662
- refresh materialized view public.order_state;
663
- -- Bloqueia SELECT na MV até terminar — minutos em MVs grandes
664
- ```
665
-
666
- **Por quê:** refresh exclusivo locka a MV. Leitores ficam bloqueados.
667
-
668
- **Certo:** CONCURRENTLY + unique index na MV:
669
-
670
- ```sql
671
- -- ✅ Unique index obrigatório para CONCURRENTLY
672
- create unique index on public.order_state (order_id);
673
-
674
- refresh materialized view concurrently public.order_state;
675
- -- Refresh em background; reads continuam funcionando
676
- ```
677
-
678
- ### Anti-pattern 5: Event sourcing sem snapshot → replay O(n) caro
679
-
680
- **Errado:**
681
-
682
- ```sql
683
- -- ❌ Reconstruir estado de aggregate antigo via replay completo
684
- select * from public.events
685
- where aggregate_id = $1
686
- order by id;
687
- -- Aggregate com 1M eventos → query lenta, alocação memória pesada
688
- ```
689
-
690
- **Por quê:** sem snapshot, replay para reconstruir estado é O(n) onde n = número total de eventos do aggregate. Em aggregates antigos (orders de 5 anos), aggregação fica cara.
691
-
692
- **Certo:** snapshot periódico + replay incremental (REGRA #6):
693
-
694
- ```sql
695
- -- ✅ Carregar snapshot + replay apenas eventos posteriores
696
- select state from public.snapshots where aggregate_id = $1;
697
- -- Aplicar eventos com id > snapshot_lsn (poucos eventos recentes)
698
- select * from public.events
699
- where aggregate_id = $1 and id > (select snapshot_lsn from public.snapshots where aggregate_id = $1);
700
- ```
701
-
702
- ## Ver também
703
-
704
- - [_shared-dados-distribuidos/glossary.md](../_shared-dados-distribuidos/glossary.md) — termos `AMQP/JMS-style broker`, `log-based broker`, `CDC`, `event sourcing`, `exactly-once semantics`, `at-least-once semantics`, `stream-stream join`, `stream-table join`, `table-table join`, `log compaction` (seção h)
705
- - [audit-log-multi-tenant](../audit-log-multi-tenant/SKILL.md) — Phase 109 v1.21, audit_log É event sourcing semantics (REQ STREAMS-03 cross-ref ATIVO)
706
- - [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — v1.8, pgmq pattern + cleanup retention TTL
707
- - [supabase-realtime](../supabase-realtime/SKILL.md) — v1.8, broadcast como CDC stream (REQ STREAMS-02 abordagem 1)
708
- - [escolha-modelo-consistencia](../escolha-modelo-consistencia/SKILL.md) — Phase 121 (irmã), transactional outbox como base de exactly-once (REQ STREAMS-04 cross-ref ATIVO)
709
- - [supabase-database-functions](../supabase-database-functions/SKILL.md) — v1.8, security invoker + search_path canônicos
710
- - DDIA Ch 11 (Stream Processing, summary p.464) — material-fonte canônico
711
- </content>
1
+ ---
2
+ name: streams-eventos-cdc
3
+ description: Use ao implementar event stream em Supabase — diferença AMQP/JMS-style (LISTEN/NOTIFY) vs log-based (pgmq) brokers, padrões CDC via wal2json + Realtime broadcast OU pglogical → Kafka, ev…
4
+ ---
5
+
6
+ # Streams, Eventos e CDC — Brokers, Event Sourcing, Exactly-Once em Postgres
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill ao implementar pipeline event-driven em Supabase + Postgres. Trigger phrases:
11
+
12
+ - "event stream Postgres", "CDC Supabase", "wal2json + Realtime"
13
+ - "pgmq vs LISTEN/NOTIFY", "broker log-based vs AMQP"
14
+ - "event sourcing Postgres", "tabela append-only de eventos"
15
+ - "exactly-once pgmq", "dedup table idempotency"
16
+ - "stream join com janela", "stream-table CDC enrichment"
17
+ - "log compaction Postgres", "snapshot eventos"
18
+ - "projeção materialized view de eventos", "denormalization via trigger"
19
+
20
+ Esta skill **estende** [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) ao reconhecer audit_log como event sourcing parcial; [`supabase-cron-queues`](../supabase-cron-queues/SKILL.md) (v1.8) para pgmq pattern; e [`supabase-realtime`](../supabase-realtime/SKILL.md) (v1.8) para broadcast como CDC stream.
21
+
22
+ Material-fonte: *Designing Data-Intensive Applications*, Martin Kleppmann (O'Reilly 2017), capítulo 11 "Stream Processing" (linhas 17812-19637 do material extraído; summary 19408-19481). Termos canônicos PT-BR ↔ EN definidos em [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) seção (h).
23
+
24
+ ## Regras absolutas
25
+
26
+ **REGRA #1 (broker log-based default para event sourcing):** Para event sourcing, CDC ou pipeline com replay obrigatório, escolher **log-based broker** (Kafka, pgmq) — mensagem retida (TTL configurável), múltiplos consumers tracked offset independente, replay possível. AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY) deletam mensagem após ack — sem replay, single-consumer.
27
+
28
+ **REGRA #2 (CDC via wal2json + Realtime é default Supabase):** Se ambiente é Supabase + use case é sync índice/desnormalização/multi-region, default é wal2json + Supabase Realtime broadcast. Zero infra extra. Apenas considerar pglogical → Kafka externo se warehousing analítico for o uso primário.
29
+
30
+ **REGRA #3 (event sourcing exige tabela append-only + projeções derivadas):** Tabela `events` deve ser **append-only** (REVOKE DELETE/UPDATE como audit_log v1.21). Estado atual = projeção derivada via Materialized View ou trigger-maintained denormalization — NUNCA escrever direto em "tabela de estado". Source of truth = stream de eventos.
31
+
32
+ **REGRA #4 (exactly-once pgmq exige dedup + idempotency + transactional outbox):** pgmq não garante exactly-once nativo (at-least-once entrega). Para semântica exactly-once: (a) **dedup table** com `unique(event_id)` rejeitando duplicatas; (b) **handler idempotente** (mesmo input → mesmo output, sem efeitos colaterais); (c) **transactional outbox** para cross-service writes.
33
+
34
+ **REGRA #5 (stream join exige janela temporal explícita):** Stream-stream join sem janela = memória cresce sem limite (cada evento aguarda match indefinidamente). Toda janela deve ter TTL explícito (tumbling, sliding, session). Default: tumbling 5min para business events; sliding 1min para latency-sensitive.
35
+
36
+ **REGRA #6 (log compaction não-trivial em pgmq — exige snapshot manual):** pgmq não tem log compaction nativa (Kafka tem). Para event sourcing com snapshot: criar tabela `snapshots` periodicamente, deletar `events.id < snapshot_lsn` correspondente. Sem snapshot = tabela `events` cresce sem limite, replay torna-se O(n) caro.
37
+
38
+ ## Patterns canônicos
39
+
40
+ ### REQ STREAMS-01 — Brokers AMQP/JMS-style vs log-based
41
+
42
+ | Tipo | Exemplos | Mensagem após ack | Multi-consumer | Replay | Use case |
43
+ |---|---|---|---|---|---|
44
+ | **AMQP/JMS-style** | RabbitMQ, postgres `LISTEN/NOTIFY`, ActiveMQ | Deletada (consumida) | Single (work queue — distribui rounds robin) | Não (gone after ack) | Task queue async (envio email, geração PDF) |
45
+ | **Log-based** | Kafka, pgmq, Redpanda, Pulsar | Retida (TTL configurável) | Multiple (cada consumer tracks offset independente) | Sim (replay desde offset N) | Event sourcing, CDC, audit, analytics |
46
+
47
+ **Como escolher:**
48
+
49
+ ```
50
+ Use case precisa de replay? ─── Sim ──► log-based (pgmq, Kafka)
51
+
52
+ Não
53
+
54
+
55
+ Múltiplos consumers veem mesma mensagem? ─── Sim ──► log-based
56
+
57
+ Não
58
+
59
+
60
+ Mensagem é "task" descartável após processada? ─── Sim ──► AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY)
61
+
62
+ Não
63
+
64
+
65
+ Default (event-driven em B2B SaaS): log-based (pgmq)
66
+ ```
67
+
68
+ **Exemplo postgres LISTEN/NOTIFY (AMQP-style — single consumer, sem replay):**
69
+
70
+ ```sql
71
+ -- Producer
72
+ notify ch_orders, '{"order_id":"abc-123","status":"paid"}';
73
+
74
+ -- Consumer (Edge Function)
75
+ listen ch_orders;
76
+ -- Sleep até receber notification — single consumer recebe, mensagem some
77
+ ```
78
+
79
+ **Exemplo pgmq (log-based — multi-consumer, replay):**
80
+
81
+ ```sql
82
+ -- Setup (uma vez)
83
+ select pgmq.create('orders');
84
+
85
+ -- Producer
86
+ select pgmq.send('orders', '{"order_id":"abc-123","status":"paid"}');
87
+
88
+ -- Consumer 1 (worker A)
89
+ select * from pgmq.read('orders', 30, 1);
90
+ -- vt=30s (visibility timeout), 1 mensagem por leitura
91
+ -- Após ler: mensagem fica invisível por 30s — outro worker não pega
92
+ -- Worker A processa e dá ack:
93
+ select pgmq.delete('orders', msg_id);
94
+ -- Sem ack em 30s → mensagem volta à queue (at-least-once)
95
+
96
+ -- Consumer 2 (worker B / archive)
97
+ -- Se queue tem retention, archive table mantém histórico para replay
98
+ select * from pgmq.archive('orders', msg_id);
99
+ -- Mensagens em archive são replayable
100
+ ```
101
+
102
+ ### REQ STREAMS-02 — 3 padrões CDC em Postgres
103
+
104
+ CDC (Change Data Capture) = capturar mudanças no DB como stream de eventos. 3 abordagens canônicas em Supabase:
105
+
106
+ **Abordagem 1: wal2json + Supabase Realtime broadcast** (default)
107
+
108
+ ```sql
109
+ -- Habilitar replication identity (necessário para wal2json capturar UPDATE/DELETE com colunas)
110
+ alter table public.orders replica identity full;
111
+
112
+ -- Supabase Realtime já consome WAL via wal2json internamente
113
+ -- Cliente subscreve canal específico via JS client
114
+ ```
115
+
116
+ ```typescript
117
+ // Cliente Supabase consume CDC stream via Realtime
118
+ const channel = supabase
119
+ .channel('orders-cdc', { config: { private: true } })
120
+ .on(
121
+ 'postgres_changes',
122
+ { event: '*', schema: 'public', table: 'orders' },
123
+ (payload) => {
124
+ // payload.eventType: INSERT | UPDATE | DELETE
125
+ // payload.new: nova row (INSERT/UPDATE)
126
+ // payload.old: row antiga (UPDATE/DELETE — exige replica identity full)
127
+ console.log('CDC event:', payload);
128
+ }
129
+ )
130
+ .subscribe();
131
+ ```
132
+
133
+ **Trade-offs:** zero infra extra; baixa latência (sub-segundo); RLS aplicada nas mensagens (cada cliente vê só rows permitidas). Limite: scale na ordem de milhares de subscribers por canal.
134
+
135
+ **Abordagem 2: pglogical → Kafka externo** (warehousing analítico)
136
+
137
+ ```sql
138
+ -- Em Supabase Pro+ habilitar pglogical (extensão)
139
+ create extension if not exists pglogical;
140
+
141
+ -- Setup nó provider (Postgres source)
142
+ select pglogical.create_node(
143
+ node_name := 'supabase_prod',
144
+ dsn := 'host=db.xxx.supabase.co dbname=postgres'
145
+ );
146
+
147
+ -- Replication set para tabelas que viram stream
148
+ select pglogical.create_replication_set(set_name := 'cdc_set');
149
+ select pglogical.replication_set_add_table('cdc_set', 'public.orders', synchronize_data := false);
150
+
151
+ -- Conector Kafka (Debezium ou similar) consome pglogical → publica em Kafka topic
152
+ -- Trade-off: requer infra Kafka externa, latência maior (segundos), throughput muito maior
153
+ ```
154
+
155
+ **Abordagem 3: Trigger-based** (casos custom onde wal2json não cobre)
156
+
157
+ ```sql
158
+ -- Trigger que emite evento custom quando flag X muda
159
+ create or replace function public.emit_lead_qualified_event()
160
+ returns trigger
161
+ language plpgsql
162
+ security invoker
163
+ set search_path = ''
164
+ as $$
165
+ begin
166
+ if old.stage != 'qualified' and new.stage = 'qualified' then
167
+ insert into public.outbox (event_type, payload)
168
+ values (
169
+ 'lead_qualified',
170
+ jsonb_build_object(
171
+ 'lead_id', new.id,
172
+ 'org_id', new.org_id,
173
+ 'qualified_by', (select auth.uid()),
174
+ 'qualified_at', now()
175
+ )
176
+ );
177
+ end if;
178
+ return new;
179
+ end;
180
+ $$;
181
+
182
+ create trigger lead_qualified_trigger
183
+ after update on public.leads
184
+ for each row
185
+ execute function public.emit_lead_qualified_event();
186
+ ```
187
+
188
+ **Quando usar trigger-based:** semântica de evento mais rica que "linha mudou" (ex: business event "qualified" derivado de mudança específica). Worker async lê outbox e publica downstream.
189
+
190
+ **Use cases canônicos:**
191
+
192
+ | Use case | Abordagem recomendada |
193
+ |---|---|
194
+ | Sync índice de busca (Elasticsearch, Meilisearch) | wal2json + Realtime → função client que sincroniza |
195
+ | Desnormalização (Materialized View atualizada por evento) | Trigger-based (mais controle sobre quando refresh) |
196
+ | Sync multi-region cold standby | pglogical → Kafka → consumer remoto |
197
+ | Audit log retroativo + análise comportamental | wal2json (captura cru) → analytics warehouse |
198
+ | Notificação push (mobile app) | Realtime broadcast direto (zero step intermediário) |
199
+
200
+ ### REQ STREAMS-03 — Event sourcing em Postgres
201
+
202
+ **Princípio canônico:** eventos imutáveis são source of truth; estado atual é projeção derivada.
203
+
204
+ **Schema canônico:**
205
+
206
+ ```sql
207
+ -- Tabela events — source of truth (append-only)
208
+ create table public.events (
209
+ id bigserial primary key,
210
+ aggregate_id uuid not null, -- ID da entidade (order, user, ...)
211
+ aggregate_type text not null, -- Tipo da entidade ('order', 'user')
212
+ event_type text not null, -- 'order_created', 'order_paid', 'order_shipped'
213
+ payload jsonb not null, -- Detalhes do evento
214
+ metadata jsonb, -- actor_id, request_id, trace_id
215
+ created_at timestamptz not null default now()
216
+ );
217
+
218
+ -- Index canônico (para reproduzir histórico de uma entidade)
219
+ create index events_aggregate_idx on public.events (aggregate_id, id);
220
+
221
+ -- Index para query por tipo (analytics)
222
+ create index events_type_created_idx on public.events (event_type, created_at);
223
+
224
+ -- REGRA #3 — append-only: REVOKE DELETE/UPDATE
225
+ revoke delete, update on public.events from public, authenticated, anon, service_role;
226
+ -- Apenas postgres role pode deletar (cleanup com snapshot)
227
+ ```
228
+
229
+ **Cross-ref ATIVO** para [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) — audit_log É event sourcing semantics: append-only, imutável, retém histórico cronológico. Quem implementou audit_log já fez event sourcing parcial.
230
+
231
+ **Projeção via Materialized View:**
232
+
233
+ ```sql
234
+ -- Projeção: estado atual de cada order derivado dos eventos
235
+ create materialized view public.order_state as
236
+ select
237
+ aggregate_id as order_id,
238
+ -- Reconstrói estado a partir dos eventos (último win)
239
+ (array_agg(payload->>'status' order by id desc))[1] as current_status,
240
+ (array_agg(payload->>'total' order by id desc))[1]::numeric as current_total,
241
+ min(created_at) as created_at,
242
+ max(created_at) as updated_at,
243
+ count(*) as event_count
244
+ from public.events
245
+ where aggregate_type = 'order'
246
+ group by aggregate_id;
247
+
248
+ create unique index on public.order_state (order_id);
249
+
250
+ -- Refresh (incremental via concurrent OR full)
251
+ refresh materialized view concurrently public.order_state;
252
+ -- Ou via pg_cron a cada N minutos
253
+ ```
254
+
255
+ **Projeção via trigger-maintained denormalization:**
256
+
257
+ ```sql
258
+ -- Tabela de estado mantida por trigger (atualizada por cada novo evento)
259
+ create table public.order_current_state (
260
+ order_id uuid primary key,
261
+ status text not null,
262
+ total numeric,
263
+ updated_at timestamptz not null default now()
264
+ );
265
+
266
+ create or replace function public.project_order_event()
267
+ returns trigger
268
+ language plpgsql
269
+ security invoker
270
+ set search_path = ''
271
+ as $$
272
+ begin
273
+ if new.aggregate_type = 'order' then
274
+ insert into public.order_current_state (order_id, status, total, updated_at)
275
+ values (
276
+ new.aggregate_id,
277
+ new.payload->>'status',
278
+ (new.payload->>'total')::numeric,
279
+ new.created_at
280
+ )
281
+ on conflict (order_id) do update
282
+ set status = excluded.status,
283
+ total = coalesce(excluded.total, public.order_current_state.total),
284
+ updated_at = excluded.updated_at;
285
+ end if;
286
+ return new;
287
+ end;
288
+ $$;
289
+
290
+ create trigger project_order_event_trigger
291
+ after insert on public.events
292
+ for each row
293
+ execute function public.project_order_event();
294
+ ```
295
+
296
+ **Quando MV vs trigger:**
297
+
298
+ | Critério | MV concurrent refresh | Trigger denormalization |
299
+ |---|---|---|
300
+ | **Latência** | Periódica (minutos) | Imediata (no commit do evento) |
301
+ | **Custo write** | Baixo (write apenas em events) | Alto (write em events + state) |
302
+ | **Custo read** | Baixo (state já agregado) | Baixo |
303
+ | **Use case** | Analytics, dashboards | UI real-time, business state |
304
+
305
+ ### REQ STREAMS-04 — Exactly-once em pgmq
306
+
307
+ pgmq oferece **at-least-once** nativo (mensagem reenviada se worker crash sem ack). Para semântica **exactly-once**, combinação de 3 técnicas:
308
+
309
+ **Técnica 1: Dedup table com unique(event_id)**
310
+
311
+ ```sql
312
+ -- Tabela de eventos já processados
313
+ create table public.processed_events (
314
+ event_id uuid primary key,
315
+ processed_at timestamptz not null default now(),
316
+ processor text not null -- nome do worker para debug
317
+ );
318
+ ```
319
+
320
+ **Técnica 2: Handler atomic — INSERT na dedup + processamento na MESMA transação**
321
+
322
+ ```sql
323
+ -- Worker (Edge Function ou função PG)
324
+ create or replace function public.process_order_event(p_msg_id bigint)
325
+ returns void
326
+ language plpgsql
327
+ security definer -- worker tem privilégios elevados
328
+ set search_path = ''
329
+ as $$
330
+ declare
331
+ v_msg record;
332
+ v_event_id uuid;
333
+ begin
334
+ -- Lê mensagem da queue com visibility timeout
335
+ select msg_id, message into v_msg
336
+ from pgmq.read('orders', 30, 1)
337
+ limit 1;
338
+
339
+ if v_msg is null then return; end if;
340
+
341
+ v_event_id := (v_msg.message->>'event_id')::uuid;
342
+
343
+ begin
344
+ -- Atomic: INSERT dedup + processamento
345
+ insert into public.processed_events (event_id, processor)
346
+ values (v_event_id, 'process_order_event');
347
+ -- Falha (unique violation) se já processado → exception abort tudo
348
+
349
+ -- ... lógica de processamento idempotente ...
350
+ update public.orders set status = 'paid' where id = (v_msg.message->>'order_id')::uuid;
351
+
352
+ -- Ack — remove da queue
353
+ perform pgmq.delete('orders', v_msg.msg_id);
354
+
355
+ exception when unique_violation then
356
+ -- Já processado — apenas dar ack para remover da queue
357
+ perform pgmq.delete('orders', v_msg.msg_id);
358
+ end;
359
+ end;
360
+ $$;
361
+ ```
362
+
363
+ **Técnica 3: Idempotency key no handler — mesmo input → mesmo output (sem efeitos colaterais)**
364
+
365
+ Idempotency = processar a mesma mensagem N vezes produz o mesmo resultado. Padrões:
366
+
367
+ ```sql
368
+ -- Idempotente via UPDATE condicional (não muda se já está no estado)
369
+ update public.orders
370
+ set status = 'paid'
371
+ where id = $1 and status != 'paid';
372
+ -- Se já 'paid' → no-op, RETURNING vazio
373
+
374
+ -- Idempotente via INSERT ON CONFLICT
375
+ insert into public.payments (order_id, amount, transaction_id)
376
+ values ($1, $2, $3)
377
+ on conflict (transaction_id) do nothing;
378
+ -- Mesmo transaction_id → no-op
379
+ ```
380
+
381
+ **Cross-ref ATIVO** para [`escolha-modelo-consistencia`](../escolha-modelo-consistencia/SKILL.md) — pattern transactional outbox descrito lá é a base de exactly-once entre DB e broker (write atomic em mesma transação).
382
+
383
+ ### REQ STREAMS-05 — 3 tipos de stream join com SQL exemplo
384
+
385
+ **Tipo 1: Stream-stream join (com janela temporal)**
386
+
387
+ Match de eventos de 2 streams dentro de uma janela. Ex: matching pedido + pagamento dentro de 5min via tumbling window.
388
+
389
+ ```sql
390
+ -- Materialização: 2 tabelas event log + JOIN com window
391
+ create table public.order_events (
392
+ order_id uuid not null,
393
+ event_at timestamptz not null,
394
+ event_type text not null,
395
+ payload jsonb
396
+ );
397
+
398
+ create table public.payment_events (
399
+ payment_id uuid not null,
400
+ order_id uuid not null,
401
+ event_at timestamptz not null,
402
+ amount numeric
403
+ );
404
+
405
+ -- Stream-stream join via tumbling window 5min
406
+ create or replace view public.order_payment_join_5min as
407
+ select
408
+ o.order_id,
409
+ o.event_at as order_at,
410
+ p.event_at as paid_at,
411
+ p.amount,
412
+ date_trunc('minute', o.event_at) as window_start
413
+ from public.order_events o
414
+ join public.payment_events p on p.order_id = o.order_id
415
+ where o.event_type = 'order_created'
416
+ and p.event_at between o.event_at and o.event_at + interval '5 minutes'
417
+ order by o.event_at;
418
+ ```
419
+
420
+ **Trade-off:** janela tumbling = não-overlapping, mais simples; sliding = overlapping, mais alertas; session = dinâmica, agrupada por user activity.
421
+
422
+ **Tipo 2: Stream-table join (CDC + atividade — enrichment)**
423
+
424
+ Stream de eventos enriquecido com lookup em tabela de referência atualizada por CDC.
425
+
426
+ ```sql
427
+ -- Tabela users mantida atualizada via CDC (Realtime ou trigger)
428
+ -- Stream de eventos: clicks, logins, purchases — precisa enriched com user info
429
+
430
+ select
431
+ e.event_id,
432
+ e.event_type,
433
+ e.event_at,
434
+ -- Enrichment: lookup do user no momento atual (não do momento do evento)
435
+ u.email,
436
+ u.tier,
437
+ u.country
438
+ from public.user_events e
439
+ join public.users u on u.id = e.user_id
440
+ where e.event_at > now() - interval '1 hour';
441
+
442
+ -- Para latência baixa: keep tabela users em memória do worker (CDC stream → cache)
443
+ ```
444
+
445
+ **Cuidado canônico:** se a tabela mudou desde o evento, enrichment usa o estado **atual** do user, não o estado **no momento do evento**. Para histórico fiel: capturar snapshot no payload do evento (ex: `payload.user_email_at_event`).
446
+
447
+ **Tipo 3: Table-table join (merge de changelogs CDC)**
448
+
449
+ Merge de 2 changelogs CDC para produzir view denormalizada. Ex: orders changelog + customers changelog → view denormalizada de pedidos com info do cliente.
450
+
451
+ ```sql
452
+ -- Materialized view derivada de 2 streams CDC mergeados
453
+ create materialized view public.orders_denorm as
454
+ select
455
+ o.order_id,
456
+ o.status,
457
+ o.total,
458
+ o.created_at as order_created_at,
459
+ c.email as customer_email,
460
+ c.tier as customer_tier,
461
+ c.country as customer_country
462
+ from public.orders o
463
+ join public.customers c on c.id = o.customer_id;
464
+
465
+ create unique index on public.orders_denorm (order_id);
466
+
467
+ -- Refresh disparado por CDC events em orders OU customers
468
+ create or replace function public.refresh_orders_denorm()
469
+ returns trigger
470
+ language plpgsql
471
+ as $$
472
+ begin
473
+ refresh materialized view concurrently public.orders_denorm;
474
+ return null;
475
+ end;
476
+ $$;
477
+
478
+ create trigger orders_changelog_trigger
479
+ after insert or update on public.orders
480
+ for each statement
481
+ execute function public.refresh_orders_denorm();
482
+
483
+ create trigger customers_changelog_trigger
484
+ after update on public.customers
485
+ for each statement
486
+ execute function public.refresh_orders_denorm();
487
+ ```
488
+
489
+ **Trade-off:** refresh CONCURRENTLY exige unique index, latência maior. Para tabelas grandes, usar incremental refresh via trigger denormalization (REQ STREAMS-03).
490
+
491
+ ### REQ STREAMS-06 — Log compaction strategy
492
+
493
+ Log compaction = para cada chave, manter apenas o último valor. Reduz storage sem perder estado atual.
494
+
495
+ **pgmq não tem nativa** — usa retention TTL via `vacuum_archive`:
496
+
497
+ ```sql
498
+ -- pgmq archive movido para tabela archive periodicamente
499
+ select pgmq.archive('orders', 12345);
500
+ -- Após N dias na archive, vacuum_archive deleta hard
501
+
502
+ -- Configurar TTL via pg_cron
503
+ select cron.schedule(
504
+ 'pgmq_vacuum_archive',
505
+ '0 2 * * *',
506
+ $$ select pgmq.purge_archive('orders', 30); $$
507
+ -- Deleta da archive mensagens > 30 dias
508
+ );
509
+ ```
510
+
511
+ **Event sourcing exige snapshot periódico + compact:**
512
+
513
+ ```sql
514
+ -- Tabela de snapshots — estado materializado a cada N eventos
515
+ create table public.snapshots (
516
+ aggregate_id uuid primary key,
517
+ snapshot_lsn bigint not null, -- até qual event.id este snapshot reflete
518
+ state jsonb not null, -- estado serializado
519
+ created_at timestamptz not null default now()
520
+ );
521
+
522
+ -- Função: criar snapshot para um aggregate quando event_count > threshold
523
+ create or replace function public.create_snapshot(p_aggregate_id uuid)
524
+ returns void
525
+ language plpgsql
526
+ security invoker
527
+ set search_path = ''
528
+ as $$
529
+ declare
530
+ v_state jsonb;
531
+ v_snapshot_lsn bigint;
532
+ begin
533
+ -- Reproduzir todos os eventos para construir estado atual
534
+ select
535
+ jsonb_build_object(
536
+ 'status', (array_agg(payload->>'status' order by id desc))[1],
537
+ 'total', (array_agg(payload->>'total' order by id desc))[1]::numeric,
538
+ 'event_count', count(*)
539
+ ),
540
+ max(id)
541
+ into v_state, v_snapshot_lsn
542
+ from public.events
543
+ where aggregate_id = p_aggregate_id;
544
+
545
+ -- Salvar snapshot (insert or update)
546
+ insert into public.snapshots (aggregate_id, snapshot_lsn, state)
547
+ values (p_aggregate_id, v_snapshot_lsn, v_state)
548
+ on conflict (aggregate_id) do update
549
+ set snapshot_lsn = excluded.snapshot_lsn,
550
+ state = excluded.state,
551
+ created_at = now();
552
+ end;
553
+ $$;
554
+
555
+ -- Compact: deletar eventos < snapshot_lsn (tomados em consideração no snapshot)
556
+ -- ATENÇÃO: requer privilégio especial (REGRA #3 — REVOKE DELETE em events)
557
+ -- Apenas postgres role + função SECURITY DEFINER
558
+ create or replace function public.compact_aggregate_events(p_aggregate_id uuid)
559
+ returns int
560
+ language plpgsql
561
+ security definer
562
+ set search_path = ''
563
+ as $$
564
+ declare
565
+ v_deleted int;
566
+ v_snapshot_lsn bigint;
567
+ begin
568
+ -- Confirmar que snapshot existe
569
+ select snapshot_lsn into v_snapshot_lsn
570
+ from public.snapshots
571
+ where aggregate_id = p_aggregate_id;
572
+
573
+ if v_snapshot_lsn is null then
574
+ raise exception 'Snapshot ausente para aggregate_id %', p_aggregate_id;
575
+ end if;
576
+
577
+ -- Deletar eventos antes do snapshot
578
+ delete from public.events
579
+ where aggregate_id = p_aggregate_id
580
+ and id <= v_snapshot_lsn;
581
+
582
+ get diagnostics v_deleted = row_count;
583
+ return v_deleted;
584
+ end;
585
+ $$;
586
+
587
+ revoke execute on function public.compact_aggregate_events from public, authenticated, anon;
588
+ -- Apenas service_role pode chamar
589
+ ```
590
+
591
+ **Estratégia canônica:** snapshot a cada 1000 eventos por aggregate; compact após snapshot validado (replay do snapshot reproduz estado atual). Sem snapshot/compact, replay para reconstruir estado torna-se O(n) caro em aggregates antigos.
592
+
593
+ ## Anti-patterns
594
+
595
+ ### Anti-pattern 1: Usar LISTEN/NOTIFY para event sourcing
596
+
597
+ **Errado:**
598
+
599
+ ```sql
600
+ -- ❌ LISTEN/NOTIFY como "event log"
601
+ notify ch_orders, '{"order_id":"abc","event":"paid"}';
602
+ -- Consumer offline → mensagem perdida
603
+ -- Sem replay, sem multi-consumer
604
+ ```
605
+
606
+ **Por quê:** LISTEN/NOTIFY é AMQP/JMS-style — single consumer ativo recebe, mensagem some. Se consumer offline durante notify, evento perdido. Sem replay.
607
+
608
+ **Certo:** pgmq (log-based) ou tabela `events` append-only para event sourcing (REGRA #1).
609
+
610
+ ### Anti-pattern 2: Event sourcing sem dedup → eventos duplicados
611
+
612
+ **Errado:**
613
+
614
+ ```sql
615
+ -- ❌ Worker pgmq processa sem dedup table
616
+ create or replace function public.process_event(p_msg jsonb)
617
+ returns void
618
+ language plpgsql
619
+ as $$
620
+ begin
621
+ -- Processa direto, sem checar se já processado
622
+ update public.orders set status = 'paid' where id = (p_msg->>'order_id')::uuid;
623
+ -- Se mensagem reentregue (worker crash + redelivery) → status setado 2×
624
+ -- Se webhook externo → cobra cliente 2×
625
+ end;
626
+ $$;
627
+ ```
628
+
629
+ **Por quê:** pgmq é at-least-once. Mensagem pode ser entregue >1× (worker crash sem ack, visibility timeout expirado). Sem dedup, processamento repetido = side effect duplicado.
630
+
631
+ **Certo:** dedup table + handler idempotente (REGRA #4). Mesmo input → mesmo output.
632
+
633
+ ### Anti-pattern 3: Stream-stream join sem janela temporal
634
+
635
+ **Errado:**
636
+
637
+ ```sql
638
+ -- ❌ Sem janela temporal: memória cresce indefinidamente
639
+ select o.order_id, p.payment_id
640
+ from public.order_events o
641
+ join public.payment_events p on p.order_id = o.order_id;
642
+ -- Cada evento aguarda match indefinido — payment de 3 anos atrás casa com order recente
643
+ -- Memória do worker cresce sem limite
644
+ ```
645
+
646
+ **Por quê:** stream join sem TTL = sistema mantém eventos em memória aguardando match. Memória cresce linearmente com tempo, eventualmente OOM.
647
+
648
+ **Certo:** janela explícita (REGRA #5):
649
+
650
+ ```sql
651
+ -- ✅ Tumbling window 5min
652
+ join public.payment_events p on p.order_id = o.order_id
653
+ where p.event_at between o.event_at and o.event_at + interval '5 minutes';
654
+ ```
655
+
656
+ ### Anti-pattern 4: Materialized View sem CONCURRENTLY → bloqueio em refresh
657
+
658
+ **Errado:**
659
+
660
+ ```sql
661
+ -- ❌ refresh sem CONCURRENTLY trava reads na MV durante refresh
662
+ refresh materialized view public.order_state;
663
+ -- Bloqueia SELECT na MV até terminar — minutos em MVs grandes
664
+ ```
665
+
666
+ **Por quê:** refresh exclusivo locka a MV. Leitores ficam bloqueados.
667
+
668
+ **Certo:** CONCURRENTLY + unique index na MV:
669
+
670
+ ```sql
671
+ -- ✅ Unique index obrigatório para CONCURRENTLY
672
+ create unique index on public.order_state (order_id);
673
+
674
+ refresh materialized view concurrently public.order_state;
675
+ -- Refresh em background; reads continuam funcionando
676
+ ```
677
+
678
+ ### Anti-pattern 5: Event sourcing sem snapshot → replay O(n) caro
679
+
680
+ **Errado:**
681
+
682
+ ```sql
683
+ -- ❌ Reconstruir estado de aggregate antigo via replay completo
684
+ select * from public.events
685
+ where aggregate_id = $1
686
+ order by id;
687
+ -- Aggregate com 1M eventos → query lenta, alocação memória pesada
688
+ ```
689
+
690
+ **Por quê:** sem snapshot, replay para reconstruir estado é O(n) onde n = número total de eventos do aggregate. Em aggregates antigos (orders de 5 anos), aggregação fica cara.
691
+
692
+ **Certo:** snapshot periódico + replay incremental (REGRA #6):
693
+
694
+ ```sql
695
+ -- ✅ Carregar snapshot + replay apenas eventos posteriores
696
+ select state from public.snapshots where aggregate_id = $1;
697
+ -- Aplicar eventos com id > snapshot_lsn (poucos eventos recentes)
698
+ select * from public.events
699
+ where aggregate_id = $1 and id > (select snapshot_lsn from public.snapshots where aggregate_id = $1);
700
+ ```
701
+
702
+ ## Ver também
703
+
704
+ - [_shared-dados-distribuidos/glossary.md](../_shared-dados-distribuidos/glossary.md) — termos `AMQP/JMS-style broker`, `log-based broker`, `CDC`, `event sourcing`, `exactly-once semantics`, `at-least-once semantics`, `stream-stream join`, `stream-table join`, `table-table join`, `log compaction` (seção h)
705
+ - [audit-log-multi-tenant](../audit-log-multi-tenant/SKILL.md) — Phase 109 v1.21, audit_log É event sourcing semantics (REQ STREAMS-03 cross-ref ATIVO)
706
+ - [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — v1.8, pgmq pattern + cleanup retention TTL
707
+ - [supabase-realtime](../supabase-realtime/SKILL.md) — v1.8, broadcast como CDC stream (REQ STREAMS-02 abordagem 1)
708
+ - [escolha-modelo-consistencia](../escolha-modelo-consistencia/SKILL.md) — Phase 121 (irmã), transactional outbox como base de exactly-once (REQ STREAMS-04 cross-ref ATIVO)
709
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — v1.8, security invoker + search_path canônicos
710
+ - DDIA Ch 11 (Stream Processing, summary p.464) — material-fonte canônico
711
+ </content>
712
712
  </invoke>