@luanpdd/kit-mcp 1.30.1 → 1.31.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 (347) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/gates/agent-no-recursive-dispatch.md +84 -82
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/README.md +76 -76
  6. package/kit/agents/advisor-researcher.md +107 -106
  7. package/kit/agents/ai-mutation-tester.md +1 -0
  8. package/kit/agents/assumptions-analyzer.md +108 -107
  9. package/kit/agents/audit-log-implementer.md +314 -313
  10. package/kit/agents/auditor-consistencia-isolamento.md +414 -413
  11. package/kit/agents/b2b-saas-architect.md +157 -156
  12. package/kit/agents/burn-rate-forecaster.md +1 -0
  13. package/kit/agents/cascading-failures-auditor.md +299 -298
  14. package/kit/agents/codebase-mapper.md +769 -768
  15. package/kit/agents/crm-pipeline-implementer.md +257 -256
  16. package/kit/agents/debugger.md +814 -813
  17. package/kit/agents/detector-tenant-quente.md +338 -337
  18. package/kit/agents/evolution-go-integrator.md +201 -200
  19. package/kit/agents/example-reviewer.md +22 -21
  20. package/kit/agents/executor.md +565 -564
  21. package/kit/agents/golden-signals-instrumenter.md +1 -0
  22. package/kit/agents/incident-investigator.md +1 -0
  23. package/kit/agents/integration-checker.md +201 -200
  24. package/kit/agents/invite-flow-implementer.md +190 -189
  25. package/kit/agents/legacy-characterizer.md +369 -368
  26. package/kit/agents/lgpd-compliance-auditor.md +296 -295
  27. package/kit/agents/load-shedding-instrumenter.md +1 -0
  28. package/kit/agents/multi-tenant-isolation-auditor.md +254 -253
  29. package/kit/agents/multi-tenant-rls-writer.md +341 -340
  30. package/kit/agents/nyquist-auditor.md +179 -178
  31. package/kit/agents/observability-coverage-auditor.md +316 -315
  32. package/kit/agents/observability-instrumenter.md +1 -0
  33. package/kit/agents/omm-auditor.md +1 -0
  34. package/kit/agents/org-onboarding-implementer.md +224 -223
  35. package/kit/agents/payload-capture-instrumenter.md +274 -273
  36. package/kit/agents/phase-researcher.md +697 -696
  37. package/kit/agents/plan-checker.md +273 -272
  38. package/kit/agents/planner.md +923 -922
  39. package/kit/agents/postmortem-writer.md +1 -0
  40. package/kit/agents/project-researcher.md +653 -652
  41. package/kit/agents/prr-conductor.md +1 -0
  42. package/kit/agents/refactor-safety-auditor.md +405 -404
  43. package/kit/agents/release-pipeline-auditor.md +1 -0
  44. package/kit/agents/research-synthesizer.md +246 -245
  45. package/kit/agents/roadmapper.md +678 -677
  46. package/kit/agents/schema-checker.md +1 -0
  47. package/kit/agents/seam-finder.md +360 -359
  48. package/kit/agents/shotgun-surgery-detector.md +350 -349
  49. package/kit/agents/slo-engineer.md +1 -0
  50. package/kit/agents/storytelling-analyst.md +1 -0
  51. package/kit/agents/supabase-architect.md +1 -0
  52. package/kit/agents/supabase-auth-bootstrapper.md +1 -0
  53. package/kit/agents/supabase-branching-architect.md +563 -562
  54. package/kit/agents/supabase-cicd-pipeline-implementer.md +778 -777
  55. package/kit/agents/supabase-column-privileges-writer.md +400 -399
  56. package/kit/agents/supabase-edge-fn-tester.md +2 -1
  57. package/kit/agents/supabase-edge-fn-writer.md +2 -1
  58. package/kit/agents/supabase-migration-writer.md +386 -385
  59. package/kit/agents/supabase-rbac-implementer.md +393 -392
  60. package/kit/agents/supabase-realtime-implementer.md +364 -363
  61. package/kit/agents/supabase-rls-hardener.md +522 -521
  62. package/kit/agents/supabase-rls-writer.md +324 -323
  63. package/kit/agents/supabase-roles-implementer.md +356 -355
  64. package/kit/agents/supabase-storage-implementer.md +1 -0
  65. package/kit/agents/super-admin-implementer.md +282 -281
  66. package/kit/agents/toil-auditor.md +1 -0
  67. package/kit/agents/ui-auditor.md +438 -437
  68. package/kit/agents/ui-checker.md +303 -302
  69. package/kit/agents/ui-researcher.md +356 -355
  70. package/kit/agents/user-profiler.md +176 -175
  71. package/kit/agents/validador-evolucao-schema.md +336 -335
  72. package/kit/agents/verifier.md +729 -728
  73. package/kit/commands/adicionar-backlog.md +75 -75
  74. package/kit/commands/adicionar-fase.md +42 -42
  75. package/kit/commands/adicionar-tarefa.md +45 -45
  76. package/kit/commands/adicionar-testes.md +41 -41
  77. package/kit/commands/ajuda.md +21 -21
  78. package/kit/commands/atualizar.md +37 -37
  79. package/kit/commands/auditar-cascading.md +111 -111
  80. package/kit/commands/auditar-marco.md +179 -179
  81. package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
  82. package/kit/commands/auditar-refactor.md +219 -219
  83. package/kit/commands/auditar-release.md +109 -109
  84. package/kit/commands/auditar-uat.md +23 -23
  85. package/kit/commands/autonomo.md +40 -40
  86. package/kit/commands/branch-pr.md +24 -24
  87. package/kit/commands/burn-rate-status.md +408 -408
  88. package/kit/commands/capturar-payloads.md +193 -193
  89. package/kit/commands/caracterizar.md +212 -212
  90. package/kit/commands/concluir-marco.md +247 -247
  91. package/kit/commands/configuracoes.md +36 -36
  92. package/kit/commands/dados-distribuidos.md +188 -188
  93. package/kit/commands/definir-perfil.md +10 -10
  94. package/kit/commands/depurar.md +190 -190
  95. package/kit/commands/detectar-duplicacao.md +197 -197
  96. package/kit/commands/discutir-fase.md +131 -131
  97. package/kit/commands/encontrar-seams.md +136 -136
  98. package/kit/commands/entrar-discord.md +17 -17
  99. package/kit/commands/estatisticas.md +18 -18
  100. package/kit/commands/example-greeting.md +33 -33
  101. package/kit/commands/executar-fase.md +58 -58
  102. package/kit/commands/expresso.md +56 -56
  103. package/kit/commands/fase-ui.md +34 -34
  104. package/kit/commands/fazer.md +57 -57
  105. package/kit/commands/fio.md +125 -125
  106. package/kit/commands/fluxos-trabalho.md +64 -64
  107. package/kit/commands/forense.md +176 -176
  108. package/kit/commands/gerenciador.md +38 -38
  109. package/kit/commands/inserir-fase.md +31 -31
  110. package/kit/commands/legacy.md +263 -263
  111. package/kit/commands/limpeza.md +17 -17
  112. package/kit/commands/listar-hipoteses-fase.md +45 -45
  113. package/kit/commands/listar-workspaces.md +18 -18
  114. package/kit/commands/load-shedding.md +117 -117
  115. package/kit/commands/mapear-codebase.md +70 -70
  116. package/kit/commands/multi-tenant.md +163 -163
  117. package/kit/commands/nota.md +33 -33
  118. package/kit/commands/novo-marco.md +43 -43
  119. package/kit/commands/novo-projeto.md +41 -41
  120. package/kit/commands/novo-workspace.md +43 -43
  121. package/kit/commands/pausar-trabalho.md +37 -37
  122. package/kit/commands/perfil-usuario.md +45 -45
  123. package/kit/commands/pesquisar-fase.md +195 -195
  124. package/kit/commands/planejar-fase.md +67 -67
  125. package/kit/commands/planejar-lacunas.md +33 -33
  126. package/kit/commands/plantar-ideia.md +25 -25
  127. package/kit/commands/progresso.md +24 -24
  128. package/kit/commands/proximo.md +30 -30
  129. package/kit/commands/publicar.md +490 -490
  130. package/kit/commands/rapido.md +35 -35
  131. package/kit/commands/reaplicar-patches.md +124 -124
  132. package/kit/commands/refactor-seguro.md +321 -321
  133. package/kit/commands/relatorio-sessao.md +19 -19
  134. package/kit/commands/remover-fase.md +31 -31
  135. package/kit/commands/remover-workspace.md +26 -26
  136. package/kit/commands/resumo-marco.md +50 -50
  137. package/kit/commands/retomar-trabalho.md +40 -40
  138. package/kit/commands/revisar-backlog.md +60 -60
  139. package/kit/commands/revisar-ui.md +32 -32
  140. package/kit/commands/revisar.md +37 -37
  141. package/kit/commands/saude.md +21 -21
  142. package/kit/commands/setup-notion.md +93 -93
  143. package/kit/commands/storytelling.md +179 -179
  144. package/kit/commands/sync-main.md +68 -68
  145. package/kit/commands/validar-fase.md +35 -35
  146. package/kit/commands/verificar-tarefas.md +44 -44
  147. package/kit/commands/verificar-trabalho.md +64 -64
  148. package/kit/file-manifest.json +82 -81
  149. package/kit/framework/bin/lib/commands.cjs +959 -959
  150. package/kit/framework/bin/lib/config.cjs +442 -442
  151. package/kit/framework/bin/lib/core.cjs +1230 -1230
  152. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  153. package/kit/framework/bin/lib/init.cjs +1442 -1442
  154. package/kit/framework/bin/lib/milestone.cjs +252 -252
  155. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  156. package/kit/framework/bin/lib/phase.cjs +888 -888
  157. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  158. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  159. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  160. package/kit/framework/bin/lib/security.cjs +382 -382
  161. package/kit/framework/bin/lib/state.cjs +1031 -1031
  162. package/kit/framework/bin/lib/template.cjs +222 -222
  163. package/kit/framework/bin/lib/uat.cjs +282 -282
  164. package/kit/framework/bin/lib/verify.cjs +888 -888
  165. package/kit/framework/bin/lib/workstream.cjs +491 -491
  166. package/kit/framework/bin/tools.cjs +918 -918
  167. package/kit/framework/commands/workstreams.md +63 -63
  168. package/kit/framework/references/checkpoints.md +778 -778
  169. package/kit/framework/references/continuation-format.md +249 -249
  170. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  171. package/kit/framework/references/git-integration.md +295 -295
  172. package/kit/framework/references/git-planning-commit.md +38 -38
  173. package/kit/framework/references/model-profile-resolution.md +36 -36
  174. package/kit/framework/references/model-profiles.md +139 -139
  175. package/kit/framework/references/phase-argument-parsing.md +61 -61
  176. package/kit/framework/references/planning-config.md +202 -202
  177. package/kit/framework/references/questioning.md +162 -162
  178. package/kit/framework/references/tdd.md +263 -263
  179. package/kit/framework/references/ui-brand.md +160 -160
  180. package/kit/framework/references/user-profiling.md +657 -657
  181. package/kit/framework/references/verification-patterns.md +612 -612
  182. package/kit/framework/references/workstream-flag.md +58 -58
  183. package/kit/framework/templates/DEBUG.md +164 -164
  184. package/kit/framework/templates/UAT.md +265 -265
  185. package/kit/framework/templates/UI-SPEC.md +100 -100
  186. package/kit/framework/templates/VALIDATION.md +76 -76
  187. package/kit/framework/templates/claude-md.md +122 -122
  188. package/kit/framework/templates/codebase/architecture.md +185 -185
  189. package/kit/framework/templates/codebase/concerns.md +205 -205
  190. package/kit/framework/templates/codebase/conventions.md +204 -204
  191. package/kit/framework/templates/codebase/integrations.md +192 -192
  192. package/kit/framework/templates/codebase/stack.md +158 -158
  193. package/kit/framework/templates/codebase/structure.md +199 -199
  194. package/kit/framework/templates/codebase/testing.md +301 -301
  195. package/kit/framework/templates/config.json +44 -44
  196. package/kit/framework/templates/context.md +352 -352
  197. package/kit/framework/templates/continue-here.md +78 -78
  198. package/kit/framework/templates/copilot-instructions.md +7 -7
  199. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  200. package/kit/framework/templates/dev-preferences.md +20 -20
  201. package/kit/framework/templates/discovery.md +146 -146
  202. package/kit/framework/templates/discussion-log.md +63 -63
  203. package/kit/framework/templates/milestone-archive.md +123 -123
  204. package/kit/framework/templates/milestone.md +115 -115
  205. package/kit/framework/templates/phase-prompt.md +610 -610
  206. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  207. package/kit/framework/templates/project.md +186 -186
  208. package/kit/framework/templates/requirements.md +231 -231
  209. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  210. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  211. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  212. package/kit/framework/templates/research-project/STACK.md +120 -120
  213. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  214. package/kit/framework/templates/research.md +419 -419
  215. package/kit/framework/templates/retrospective.md +54 -54
  216. package/kit/framework/templates/roadmap.md +202 -202
  217. package/kit/framework/templates/state.md +176 -176
  218. package/kit/framework/templates/summary-complex.md +59 -59
  219. package/kit/framework/templates/summary-minimal.md +41 -41
  220. package/kit/framework/templates/summary-standard.md +48 -48
  221. package/kit/framework/templates/summary.md +209 -209
  222. package/kit/framework/templates/user-profile.md +146 -146
  223. package/kit/framework/templates/user-setup.md +256 -256
  224. package/kit/framework/templates/verification-report.md +258 -258
  225. package/kit/framework/workflows/add-phase.md +112 -112
  226. package/kit/framework/workflows/add-tests.md +351 -351
  227. package/kit/framework/workflows/add-todo.md +158 -158
  228. package/kit/framework/workflows/audit-milestone.md +340 -340
  229. package/kit/framework/workflows/audit-uat.md +109 -109
  230. package/kit/framework/workflows/autonomous.md +891 -891
  231. package/kit/framework/workflows/check-todos.md +177 -177
  232. package/kit/framework/workflows/cleanup.md +152 -152
  233. package/kit/framework/workflows/complete-milestone.md +696 -696
  234. package/kit/framework/workflows/diagnose-issues.md +231 -231
  235. package/kit/framework/workflows/discovery-phase.md +289 -289
  236. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  237. package/kit/framework/workflows/discuss-phase.md +784 -784
  238. package/kit/framework/workflows/do.md +104 -104
  239. package/kit/framework/workflows/execute-phase.md +838 -838
  240. package/kit/framework/workflows/execute-plan.md +510 -510
  241. package/kit/framework/workflows/fast.md +102 -102
  242. package/kit/framework/workflows/forensics.md +265 -265
  243. package/kit/framework/workflows/health.md +181 -181
  244. package/kit/framework/workflows/help.md +619 -619
  245. package/kit/framework/workflows/insert-phase.md +130 -130
  246. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  247. package/kit/framework/workflows/list-workspaces.md +56 -56
  248. package/kit/framework/workflows/manager.md +362 -362
  249. package/kit/framework/workflows/map-codebase.md +377 -377
  250. package/kit/framework/workflows/milestone-summary.md +223 -223
  251. package/kit/framework/workflows/new-milestone.md +486 -486
  252. package/kit/framework/workflows/new-project.md +1159 -1159
  253. package/kit/framework/workflows/new-workspace.md +237 -237
  254. package/kit/framework/workflows/next.md +97 -97
  255. package/kit/framework/workflows/node-repair.md +92 -92
  256. package/kit/framework/workflows/note.md +156 -156
  257. package/kit/framework/workflows/pause-work.md +176 -176
  258. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  259. package/kit/framework/workflows/plan-phase.md +765 -765
  260. package/kit/framework/workflows/plant-seed.md +169 -169
  261. package/kit/framework/workflows/pr-branch.md +129 -129
  262. package/kit/framework/workflows/profile-user.md +450 -450
  263. package/kit/framework/workflows/progress.md +507 -507
  264. package/kit/framework/workflows/quick.md +757 -757
  265. package/kit/framework/workflows/remove-phase.md +155 -155
  266. package/kit/framework/workflows/remove-workspace.md +90 -90
  267. package/kit/framework/workflows/research-phase.md +82 -82
  268. package/kit/framework/workflows/resume-project.md +326 -326
  269. package/kit/framework/workflows/review.md +228 -228
  270. package/kit/framework/workflows/session-report.md +146 -146
  271. package/kit/framework/workflows/settings.md +283 -283
  272. package/kit/framework/workflows/ship.md +228 -228
  273. package/kit/framework/workflows/stats.md +60 -60
  274. package/kit/framework/workflows/transition.md +671 -671
  275. package/kit/framework/workflows/ui-phase.md +302 -302
  276. package/kit/framework/workflows/ui-review.md +165 -165
  277. package/kit/framework/workflows/update.md +323 -323
  278. package/kit/framework/workflows/validate-phase.md +174 -174
  279. package/kit/framework/workflows/verify-phase.md +252 -252
  280. package/kit/framework/workflows/verify-work.md +637 -637
  281. package/kit/hooks/check-update.js +118 -118
  282. package/kit/hooks/context-monitor.js +163 -163
  283. package/kit/hooks/kit-attribution-reminder.cjs +30 -36
  284. package/kit/hooks/kit-router.cjs +137 -0
  285. package/kit/hooks/prompt-guard.js +103 -103
  286. package/kit/hooks/statusline.js +125 -125
  287. package/kit/hooks/workflow-guard.js +101 -101
  288. package/kit/settings.json +45 -45
  289. package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
  290. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
  291. package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
  292. package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
  293. package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
  294. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
  295. package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
  296. package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
  297. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
  298. package/kit/skills/example-skill/SKILL.md +42 -42
  299. package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
  300. package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
  301. package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
  302. package/kit/skills/legacy-extract-class/SKILL.md +203 -203
  303. package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
  304. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
  305. package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
  306. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
  307. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
  308. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
  309. package/kit/skills/member-invite-flow/SKILL.md +305 -305
  310. package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
  311. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
  312. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
  313. package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
  314. package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
  315. package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
  316. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
  317. package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
  318. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
  319. package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
  320. package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
  321. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
  322. package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
  323. package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
  324. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
  325. package/kit/skills/supabase-edge-functions/SKILL.md +1 -1
  326. package/kit/skills/supabase-edge-functions-auth/SKILL.md +1 -1
  327. package/kit/skills/supabase-edge-functions-limits/SKILL.md +1 -1
  328. package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +1 -1
  329. package/kit/skills/supabase-edge-functions-testing/SKILL.md +1 -1
  330. package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +1 -1
  331. package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
  332. package/kit/skills/supabase-migrations/SKILL.md +297 -297
  333. package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
  334. package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
  335. package/kit/skills/supabase-realtime/SKILL.md +460 -460
  336. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
  337. package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
  338. package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
  339. package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
  340. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
  341. package/package.json +1 -1
  342. package/src/core/kit.js +216 -216
  343. package/src/core/reflect.js +247 -247
  344. package/src/core/reverse-sync.js +372 -372
  345. package/src/core/sync.js +437 -418
  346. package/src/core/watch.js +121 -121
  347. package/src/mcp-server/index.js +794 -715
@@ -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>