@luanpdd/kit-mcp 1.33.0 → 1.35.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 (379) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/gates/agent-no-recursive-dispatch.md +84 -84
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/COMPATIBILITY.md +70 -70
  6. package/kit/README.md +76 -76
  7. package/kit/agents/advisor-researcher.md +109 -109
  8. package/kit/agents/ai-mutation-tester.md +289 -289
  9. package/kit/agents/assumptions-analyzer.md +110 -110
  10. package/kit/agents/audit-log-implementer.md +314 -314
  11. package/kit/agents/auditor-consistencia-isolamento.md +414 -414
  12. package/kit/agents/b2b-saas-architect.md +157 -157
  13. package/kit/agents/burn-rate-forecaster.md +153 -153
  14. package/kit/agents/cascading-failures-auditor.md +299 -299
  15. package/kit/agents/codebase-mapper.md +769 -769
  16. package/kit/agents/crm-pipeline-implementer.md +257 -257
  17. package/kit/agents/debugger.md +814 -814
  18. package/kit/agents/designer-ui.md +216 -216
  19. package/kit/agents/detector-tenant-quente.md +338 -338
  20. package/kit/agents/evolution-go-integrator.md +201 -201
  21. package/kit/agents/example-reviewer.md +22 -22
  22. package/kit/agents/executor.md +565 -565
  23. package/kit/agents/golden-signals-instrumenter.md +232 -232
  24. package/kit/agents/incident-investigator.md +238 -238
  25. package/kit/agents/integration-checker.md +203 -203
  26. package/kit/agents/invite-flow-implementer.md +190 -190
  27. package/kit/agents/legacy-characterizer.md +369 -369
  28. package/kit/agents/lgpd-compliance-auditor.md +296 -296
  29. package/kit/agents/load-shedding-instrumenter.md +290 -290
  30. package/kit/agents/multi-tenant-isolation-auditor.md +254 -254
  31. package/kit/agents/multi-tenant-rls-writer.md +341 -341
  32. package/kit/agents/nyquist-auditor.md +181 -181
  33. package/kit/agents/observability-coverage-auditor.md +316 -316
  34. package/kit/agents/observability-instrumenter.md +191 -191
  35. package/kit/agents/omm-auditor.md +291 -291
  36. package/kit/agents/org-onboarding-implementer.md +224 -224
  37. package/kit/agents/payload-capture-instrumenter.md +274 -274
  38. package/kit/agents/phase-researcher.md +697 -697
  39. package/kit/agents/plan-checker.md +275 -275
  40. package/kit/agents/planner.md +923 -923
  41. package/kit/agents/postmortem-writer.md +273 -273
  42. package/kit/agents/project-researcher.md +653 -653
  43. package/kit/agents/prr-conductor.md +287 -287
  44. package/kit/agents/refactor-safety-auditor.md +405 -405
  45. package/kit/agents/release-pipeline-auditor.md +364 -364
  46. package/kit/agents/research-synthesizer.md +246 -246
  47. package/kit/agents/roadmapper.md +678 -678
  48. package/kit/agents/schema-checker.md +160 -160
  49. package/kit/agents/seam-finder.md +360 -360
  50. package/kit/agents/shotgun-surgery-detector.md +350 -350
  51. package/kit/agents/slo-engineer.md +217 -217
  52. package/kit/agents/storytelling-analyst.md +300 -300
  53. package/kit/agents/supabase-architect.md +249 -249
  54. package/kit/agents/supabase-auth-bootstrapper.md +400 -400
  55. package/kit/agents/supabase-auth-hook-writer.md +418 -418
  56. package/kit/agents/supabase-branching-architect.md +563 -563
  57. package/kit/agents/supabase-cicd-pipeline-implementer.md +778 -778
  58. package/kit/agents/supabase-column-privileges-writer.md +400 -400
  59. package/kit/agents/supabase-edge-fn-tester.md +288 -288
  60. package/kit/agents/supabase-edge-fn-writer.md +341 -341
  61. package/kit/agents/supabase-mfa-implementer.md +439 -439
  62. package/kit/agents/supabase-migration-writer.md +386 -386
  63. package/kit/agents/supabase-oauth-server-implementer.md +507 -507
  64. package/kit/agents/supabase-rbac-implementer.md +393 -393
  65. package/kit/agents/supabase-realtime-implementer.md +364 -364
  66. package/kit/agents/supabase-rls-hardener.md +522 -522
  67. package/kit/agents/supabase-rls-writer.md +324 -324
  68. package/kit/agents/supabase-roles-implementer.md +356 -356
  69. package/kit/agents/supabase-social-auth-implementer.md +451 -451
  70. package/kit/agents/supabase-sso-saml-architect.md +549 -549
  71. package/kit/agents/supabase-storage-implementer.md +407 -407
  72. package/kit/agents/super-admin-implementer.md +282 -282
  73. package/kit/agents/toil-auditor.md +268 -268
  74. package/kit/agents/ui-auditor.md +438 -438
  75. package/kit/agents/ui-checker.md +305 -305
  76. package/kit/agents/ui-researcher.md +356 -356
  77. package/kit/agents/user-profiler.md +176 -176
  78. package/kit/agents/validador-evolucao-schema.md +336 -336
  79. package/kit/agents/verifier.md +729 -729
  80. package/kit/agents/workflow-generator.md +167 -0
  81. package/kit/commands/adicionar-backlog.md +75 -75
  82. package/kit/commands/adicionar-fase.md +42 -42
  83. package/kit/commands/adicionar-tarefa.md +45 -45
  84. package/kit/commands/adicionar-testes.md +41 -41
  85. package/kit/commands/ajuda.md +21 -21
  86. package/kit/commands/atualizar.md +37 -37
  87. package/kit/commands/auditar-cascading.md +111 -111
  88. package/kit/commands/auditar-marco.md +179 -179
  89. package/kit/commands/auditar-observabilidade-cobertura-workflow.md +121 -0
  90. package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
  91. package/kit/commands/auditar-refactor.md +219 -219
  92. package/kit/commands/auditar-release.md +109 -109
  93. package/kit/commands/auditar-uat.md +23 -23
  94. package/kit/commands/autonomo.md +40 -40
  95. package/kit/commands/branch-pr.md +24 -24
  96. package/kit/commands/burn-rate-status.md +408 -408
  97. package/kit/commands/capturar-payloads.md +193 -193
  98. package/kit/commands/caracterizar.md +212 -212
  99. package/kit/commands/concluir-marco.md +247 -247
  100. package/kit/commands/configuracoes.md +36 -36
  101. package/kit/commands/criar-workflow.md +158 -0
  102. package/kit/commands/dados-distribuidos.md +188 -188
  103. package/kit/commands/definir-perfil.md +10 -10
  104. package/kit/commands/depurar.md +190 -190
  105. package/kit/commands/detectar-duplicacao.md +197 -197
  106. package/kit/commands/discutir-fase.md +131 -131
  107. package/kit/commands/encontrar-seams.md +136 -136
  108. package/kit/commands/entrar-discord.md +17 -17
  109. package/kit/commands/estatisticas.md +18 -18
  110. package/kit/commands/example-greeting.md +33 -33
  111. package/kit/commands/executar-fase.md +58 -58
  112. package/kit/commands/expresso.md +56 -56
  113. package/kit/commands/fase-ui.md +34 -34
  114. package/kit/commands/fazer.md +57 -57
  115. package/kit/commands/fio.md +125 -125
  116. package/kit/commands/fluxos-trabalho.md +64 -64
  117. package/kit/commands/forense.md +176 -176
  118. package/kit/commands/gerenciador.md +38 -38
  119. package/kit/commands/inserir-fase.md +31 -31
  120. package/kit/commands/legacy.md +263 -263
  121. package/kit/commands/limpeza.md +17 -17
  122. package/kit/commands/listar-hipoteses-fase.md +45 -45
  123. package/kit/commands/listar-workspaces.md +18 -18
  124. package/kit/commands/load-shedding.md +117 -117
  125. package/kit/commands/mapear-codebase.md +70 -70
  126. package/kit/commands/multi-tenant.md +163 -163
  127. package/kit/commands/nota.md +33 -33
  128. package/kit/commands/novo-marco.md +43 -43
  129. package/kit/commands/novo-projeto.md +41 -41
  130. package/kit/commands/novo-workspace.md +43 -43
  131. package/kit/commands/pausar-trabalho.md +37 -37
  132. package/kit/commands/perfil-usuario.md +45 -45
  133. package/kit/commands/pesquisar-fase.md +195 -195
  134. package/kit/commands/planejar-fase.md +67 -67
  135. package/kit/commands/planejar-lacunas.md +33 -33
  136. package/kit/commands/plantar-ideia.md +25 -25
  137. package/kit/commands/progresso.md +24 -24
  138. package/kit/commands/proximo.md +30 -30
  139. package/kit/commands/publicar.md +490 -490
  140. package/kit/commands/rapido.md +35 -35
  141. package/kit/commands/reaplicar-patches.md +124 -124
  142. package/kit/commands/refactor-seguro.md +321 -321
  143. package/kit/commands/relatorio-sessao.md +19 -19
  144. package/kit/commands/remover-fase.md +31 -31
  145. package/kit/commands/remover-workspace.md +26 -26
  146. package/kit/commands/resumo-marco.md +50 -50
  147. package/kit/commands/retomar-trabalho.md +40 -40
  148. package/kit/commands/revisar-backlog.md +60 -60
  149. package/kit/commands/revisar-ui.md +32 -32
  150. package/kit/commands/revisar.md +37 -37
  151. package/kit/commands/saude.md +21 -21
  152. package/kit/commands/setup-notion.md +93 -93
  153. package/kit/commands/storytelling.md +179 -179
  154. package/kit/commands/supabase.md +238 -238
  155. package/kit/commands/sync-main.md +68 -68
  156. package/kit/commands/validar-fase.md +35 -35
  157. package/kit/commands/verificar-tarefas.md +44 -44
  158. package/kit/commands/verificar-trabalho.md +64 -64
  159. package/kit/file-manifest.json +424 -419
  160. package/kit/framework/bin/lib/commands.cjs +959 -959
  161. package/kit/framework/bin/lib/config.cjs +442 -442
  162. package/kit/framework/bin/lib/core.cjs +1230 -1230
  163. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  164. package/kit/framework/bin/lib/init.cjs +1442 -1442
  165. package/kit/framework/bin/lib/milestone.cjs +252 -252
  166. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  167. package/kit/framework/bin/lib/phase.cjs +888 -888
  168. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  169. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  170. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  171. package/kit/framework/bin/lib/security.cjs +382 -382
  172. package/kit/framework/bin/lib/state.cjs +1031 -1031
  173. package/kit/framework/bin/lib/template.cjs +222 -222
  174. package/kit/framework/bin/lib/uat.cjs +282 -282
  175. package/kit/framework/bin/lib/verify.cjs +888 -888
  176. package/kit/framework/bin/lib/workstream.cjs +491 -491
  177. package/kit/framework/bin/tools.cjs +918 -918
  178. package/kit/framework/commands/workstreams.md +63 -63
  179. package/kit/framework/references/checkpoints.md +778 -778
  180. package/kit/framework/references/continuation-format.md +249 -249
  181. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  182. package/kit/framework/references/git-integration.md +295 -295
  183. package/kit/framework/references/git-planning-commit.md +38 -38
  184. package/kit/framework/references/model-profile-resolution.md +36 -36
  185. package/kit/framework/references/model-profiles.md +139 -139
  186. package/kit/framework/references/phase-argument-parsing.md +61 -61
  187. package/kit/framework/references/planning-config.md +202 -202
  188. package/kit/framework/references/questioning.md +162 -162
  189. package/kit/framework/references/tdd.md +263 -263
  190. package/kit/framework/references/ui-brand.md +160 -160
  191. package/kit/framework/references/user-profiling.md +657 -657
  192. package/kit/framework/references/verification-patterns.md +612 -612
  193. package/kit/framework/references/workstream-flag.md +58 -58
  194. package/kit/framework/templates/DEBUG.md +164 -164
  195. package/kit/framework/templates/UAT.md +265 -265
  196. package/kit/framework/templates/UI-SPEC.md +100 -100
  197. package/kit/framework/templates/VALIDATION.md +76 -76
  198. package/kit/framework/templates/claude-md.md +122 -122
  199. package/kit/framework/templates/codebase/architecture.md +185 -185
  200. package/kit/framework/templates/codebase/concerns.md +205 -205
  201. package/kit/framework/templates/codebase/conventions.md +204 -204
  202. package/kit/framework/templates/codebase/integrations.md +192 -192
  203. package/kit/framework/templates/codebase/stack.md +158 -158
  204. package/kit/framework/templates/codebase/structure.md +199 -199
  205. package/kit/framework/templates/codebase/testing.md +301 -301
  206. package/kit/framework/templates/config.json +44 -44
  207. package/kit/framework/templates/context.md +352 -352
  208. package/kit/framework/templates/continue-here.md +78 -78
  209. package/kit/framework/templates/copilot-instructions.md +7 -7
  210. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  211. package/kit/framework/templates/dev-preferences.md +20 -20
  212. package/kit/framework/templates/discovery.md +146 -146
  213. package/kit/framework/templates/discussion-log.md +63 -63
  214. package/kit/framework/templates/milestone-archive.md +123 -123
  215. package/kit/framework/templates/milestone.md +115 -115
  216. package/kit/framework/templates/phase-prompt.md +610 -610
  217. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  218. package/kit/framework/templates/project.md +186 -186
  219. package/kit/framework/templates/requirements.md +231 -231
  220. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  221. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  222. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  223. package/kit/framework/templates/research-project/STACK.md +120 -120
  224. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  225. package/kit/framework/templates/research.md +419 -419
  226. package/kit/framework/templates/retrospective.md +54 -54
  227. package/kit/framework/templates/roadmap.md +202 -202
  228. package/kit/framework/templates/state.md +176 -176
  229. package/kit/framework/templates/summary-complex.md +59 -59
  230. package/kit/framework/templates/summary-minimal.md +41 -41
  231. package/kit/framework/templates/summary-standard.md +48 -48
  232. package/kit/framework/templates/summary.md +209 -209
  233. package/kit/framework/templates/user-profile.md +146 -146
  234. package/kit/framework/templates/user-setup.md +256 -256
  235. package/kit/framework/templates/verification-report.md +258 -258
  236. package/kit/framework/workflows/add-phase.md +112 -112
  237. package/kit/framework/workflows/add-tests.md +351 -351
  238. package/kit/framework/workflows/add-todo.md +158 -158
  239. package/kit/framework/workflows/audit-milestone.md +340 -340
  240. package/kit/framework/workflows/audit-uat.md +109 -109
  241. package/kit/framework/workflows/autonomous.md +891 -891
  242. package/kit/framework/workflows/check-todos.md +177 -177
  243. package/kit/framework/workflows/cleanup.md +152 -152
  244. package/kit/framework/workflows/complete-milestone.md +696 -696
  245. package/kit/framework/workflows/diagnose-issues.md +231 -231
  246. package/kit/framework/workflows/discovery-phase.md +289 -289
  247. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  248. package/kit/framework/workflows/discuss-phase.md +784 -784
  249. package/kit/framework/workflows/do.md +104 -104
  250. package/kit/framework/workflows/execute-phase.md +838 -838
  251. package/kit/framework/workflows/execute-plan.md +510 -510
  252. package/kit/framework/workflows/fast.md +102 -102
  253. package/kit/framework/workflows/forensics.md +265 -265
  254. package/kit/framework/workflows/health.md +181 -181
  255. package/kit/framework/workflows/help.md +619 -619
  256. package/kit/framework/workflows/insert-phase.md +130 -130
  257. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  258. package/kit/framework/workflows/list-workspaces.md +56 -56
  259. package/kit/framework/workflows/manager.md +362 -362
  260. package/kit/framework/workflows/map-codebase.md +377 -377
  261. package/kit/framework/workflows/milestone-summary.md +223 -223
  262. package/kit/framework/workflows/new-milestone.md +486 -486
  263. package/kit/framework/workflows/new-project.md +1159 -1159
  264. package/kit/framework/workflows/new-workspace.md +237 -237
  265. package/kit/framework/workflows/next.md +97 -97
  266. package/kit/framework/workflows/node-repair.md +92 -92
  267. package/kit/framework/workflows/note.md +156 -156
  268. package/kit/framework/workflows/pause-work.md +176 -176
  269. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  270. package/kit/framework/workflows/plan-phase.md +765 -765
  271. package/kit/framework/workflows/plant-seed.md +169 -169
  272. package/kit/framework/workflows/pr-branch.md +129 -129
  273. package/kit/framework/workflows/profile-user.md +450 -450
  274. package/kit/framework/workflows/progress.md +507 -507
  275. package/kit/framework/workflows/quick.md +757 -757
  276. package/kit/framework/workflows/remove-phase.md +155 -155
  277. package/kit/framework/workflows/remove-workspace.md +90 -90
  278. package/kit/framework/workflows/research-phase.md +82 -82
  279. package/kit/framework/workflows/resume-project.md +326 -326
  280. package/kit/framework/workflows/review.md +228 -228
  281. package/kit/framework/workflows/session-report.md +146 -146
  282. package/kit/framework/workflows/settings.md +283 -283
  283. package/kit/framework/workflows/ship.md +228 -228
  284. package/kit/framework/workflows/stats.md +60 -60
  285. package/kit/framework/workflows/transition.md +671 -671
  286. package/kit/framework/workflows/ui-phase.md +302 -302
  287. package/kit/framework/workflows/ui-review.md +165 -165
  288. package/kit/framework/workflows/update.md +323 -323
  289. package/kit/framework/workflows/validate-phase.md +174 -174
  290. package/kit/framework/workflows/verify-phase.md +252 -252
  291. package/kit/framework/workflows/verify-work.md +637 -637
  292. package/kit/hooks/check-update.js +118 -118
  293. package/kit/hooks/context-monitor.js +163 -163
  294. package/kit/hooks/kit-attribution-reminder.cjs +92 -92
  295. package/kit/hooks/kit-router.cjs +137 -137
  296. package/kit/hooks/prompt-guard.js +103 -103
  297. package/kit/hooks/statusline.js +125 -125
  298. package/kit/hooks/workflow-guard.js +101 -101
  299. package/kit/settings.json +45 -45
  300. package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
  301. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
  302. package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
  303. package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
  304. package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
  305. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
  306. package/kit/skills/dynamic-workflow-authoring/SKILL.md +223 -0
  307. package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
  308. package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
  309. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
  310. package/kit/skills/example-skill/SKILL.md +42 -42
  311. package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
  312. package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
  313. package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
  314. package/kit/skills/legacy-extract-class/SKILL.md +203 -203
  315. package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
  316. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
  317. package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
  318. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
  319. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
  320. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
  321. package/kit/skills/member-invite-flow/SKILL.md +305 -305
  322. package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
  323. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
  324. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
  325. package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
  326. package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
  327. package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
  328. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
  329. package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
  330. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
  331. package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
  332. package/kit/skills/supabase-auth-hardening/SKILL.md +674 -674
  333. package/kit/skills/supabase-auth-hooks/SKILL.md +875 -875
  334. package/kit/skills/supabase-auth-methods/SKILL.md +486 -486
  335. package/kit/skills/supabase-auth-sessions/SKILL.md +579 -579
  336. package/kit/skills/supabase-auth-ssr/SKILL.md +306 -306
  337. package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
  338. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
  339. package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
  340. package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
  341. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
  342. package/kit/skills/supabase-edge-functions/SKILL.md +330 -330
  343. package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -309
  344. package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -302
  345. package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -279
  346. package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -277
  347. package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -357
  348. package/kit/skills/supabase-enterprise-sso-saml/SKILL.md +545 -545
  349. package/kit/skills/supabase-jwt-signing-keys/SKILL.md +399 -399
  350. package/kit/skills/supabase-mfa/SKILL.md +488 -488
  351. package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
  352. package/kit/skills/supabase-migrations/SKILL.md +297 -297
  353. package/kit/skills/supabase-oauth-server/SKILL.md +537 -537
  354. package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
  355. package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
  356. package/kit/skills/supabase-realtime/SKILL.md +460 -460
  357. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
  358. package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
  359. package/kit/skills/supabase-social-oauth/SKILL.md +480 -480
  360. package/kit/skills/supabase-third-party-auth/SKILL.md +450 -450
  361. package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
  362. package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
  363. package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -261
  364. package/kit/skills/ui-contexto-produto/SKILL.md +248 -248
  365. package/kit/skills/ui-cor-estrategia/SKILL.md +213 -213
  366. package/kit/skills/ui-critica-auditoria/SKILL.md +260 -260
  367. package/kit/skills/ui-motion-funcional/SKILL.md +264 -264
  368. package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -259
  369. package/kit/skills/ui-tipografia/SKILL.md +211 -211
  370. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
  371. package/kit/workflows/auditar-observabilidade-cobertura.workflow.js +250 -0
  372. package/package.json +65 -63
  373. package/src/core/kit.js +333 -216
  374. package/src/core/reflect.js +247 -247
  375. package/src/core/registry.js +123 -112
  376. package/src/core/reverse-sync.js +448 -372
  377. package/src/core/sync.js +477 -437
  378. package/src/core/watch.js +121 -121
  379. package/src/mcp-server/index.js +794 -794
@@ -1,605 +1,605 @@
1
- ---
2
- name: tenant-quente-mitigacao
3
- description: Use ao escalar Postgres multi-tenant em Supabase quando 1 tenant consome >>> que outros (problema "Justin Bieber tenant" do DDIA Ch 6)…
4
- ---
5
-
6
- # Tenant Quente — Mitigação (DDIA Ch 6 aplicado a Postgres + Supabase)
7
-
8
- ## Quando usar
9
-
10
- LLM carrega esta skill quando há **suspeita ou evidência de skewed workload em B2B SaaS multi-tenant** — i.e. um tenant (ou pequeno conjunto) consome desproporcionalmente recursos vs P50 dos demais. DDIA Ch 6 chama isso de **hot spot**, e o anchor narrativo canônico é o "Justin Bieber tenant" — referência ao caso Twitter onde 3% dos servidores ficaram dedicados a 1 celebrity user (DDIA p.196 nota [13]). Em B2B SaaS, o equivalente é **1 cliente enterprise** (ou anchor tenant) que escala 10× mais rápido que o restante da base.
11
-
12
- Trigger phrases:
13
-
14
- - "tenant Justin Bieber", "hot tenant", "skewed multi-tenant"
15
- - "1 cliente consumindo a base inteira", "tenant dominante", "anchor tenant"
16
- - "particionamento por tenant", "PARTITION BY HASH/RANGE org_id"
17
- - "scatter-gather Postgres super-admin"
18
- - "rebalancear tenant sem downtime", "mover tenant para schema dedicado"
19
- - "MV per-tenant pesada", "queue priority por tenant"
20
-
21
- Esta skill é consumida por `multi-tenant-isolation-auditor` (v1.21) ao detectar tabelas suspeitas de skew, por `omm-auditor` (v1.10) ao avaliar capacidade de escala, e por `b2b-saas-architect` (v1.21) ao desenhar schema de novo cliente enterprise grande.
22
-
23
- ## Regras absolutas
24
-
25
- **REGRA #1 (medir antes de mitigar):** **NUNCA** aplicar mitigação sem coletar baseline 30d das 3 métricas canônicas (REQ TENANT-01). Mitigação prematura = otimização cega. Threshold canônico: WARN >3× P50, CRITICAL >10× P50.
26
-
27
- **REGRA #2 (default document-partitioned):** Índices secundários em tabelas particionadas devem ser **document-partitioned (local)** por default. Term-partitioned (global) **só** em query path crítica onde scatter-gather é o gargalo medido.
28
-
29
- **REGRA #3 (hash quando uniforme, range quando skewed conhecido):** Particionar por `HASH (org_id)` quando workload é uniforme cross-tenant. Particionar por `RANGE (org_id)` apenas quando hot tenants são **conhecidos a priori** (anchor tenant enterprise onboarded com SLA dedicado).
30
-
31
- **REGRA #4 (rebalanceamento manual, nunca automático):** Mover tenant para schema/instância dedicada **NUNCA** automaticamente. Sempre humano-no-loop com janela de manutenção comunicada — DDIA p.204 ("Operations: automatic or manual rebalancing") documenta o risco de cascading failure quando rebalance auto reage a node lento.
32
-
33
- **REGRA #5 (cleanup conservador):** Após mover tenant, **NUNCA** dropar schema/dados antigos antes de **7d sem queries** confirmados via `pg_stat_user_tables.last_seq_scan` + `last_idx_scan`. Defesa contra rollback emergencial.
34
-
35
- ## Patterns canônicos
36
-
37
- ### REQ TENANT-01 — Detecção do "tenant Justin Bieber"
38
-
39
- Três métricas canônicas, todas com baseline 30d e threshold relativo ao P50 da base de tenants ativos:
40
-
41
- #### Métrica 1 — Ratio queries/min via `pg_stat_statements`
42
-
43
- ```sql
44
- -- Pré-requisito: pg_stat_statements habilitado (Supabase: Settings → Database → Extensions)
45
- -- Helper: extrai org_id do parameter da query (assume RLS sempre filtra por org_id literal/parameter)
46
- create or replace function private.extract_org_id_from_query(p_query text)
47
- returns uuid
48
- language plpgsql
49
- immutable
50
- set search_path = ''
51
- as $$
52
- declare
53
- m text[];
54
- begin
55
- -- Casa UUID em formato canônico no texto da query (parameter-bound)
56
- m := regexp_match(p_query, '''([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})''');
57
- if m is null then
58
- return null;
59
- end if;
60
- return m[1]::uuid;
61
- end;
62
- $$;
63
-
64
- -- View canônica: queries/min por org_id sobre janela 24h
65
- create or replace view private.hot_tenant_query_rate as
66
- with per_org as (
67
- select
68
- private.extract_org_id_from_query(query) as org_id,
69
- sum(calls) / nullif(extract(epoch from (now() - stats_reset)) / 60, 0) as queries_per_min
70
- from pg_stat_statements
71
- where private.extract_org_id_from_query(query) is not null
72
- group by 1
73
- ),
74
- stats as (
75
- select
76
- percentile_cont(0.5) within group (order by queries_per_min) as p50
77
- from per_org
78
- )
79
- select
80
- per_org.org_id,
81
- per_org.queries_per_min,
82
- stats.p50,
83
- round((per_org.queries_per_min / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
84
- case
85
- when per_org.queries_per_min > 10 * stats.p50 then 'CRITICAL'
86
- when per_org.queries_per_min > 3 * stats.p50 then 'WARN'
87
- else 'OK'
88
- end as severity
89
- from per_org cross join stats
90
- order by ratio_vs_p50 desc nulls last;
91
- ```
92
-
93
- #### Métrica 2 — Ratio storage GB via `pg_total_relation_size`
94
-
95
- ```sql
96
- -- View: storage por tenant agregando tabelas particionadas + tabelas não-particionadas
97
- -- Assume convenção de naming partição: <tabela_base>_<org_id_underscore>
98
- create or replace view private.hot_tenant_storage as
99
- with per_partition as (
100
- select
101
- -- Extrai org_id do nome da partição (audit_logs_<uuid_underscore> -> uuid)
102
- replace(
103
- regexp_replace(c.relname, '^[a-z_]+_([0-9a-f_]{36})$', '\1'),
104
- '_', '-'
105
- )::uuid as org_id,
106
- pg_total_relation_size(c.oid) as bytes
107
- from pg_class c
108
- join pg_namespace n on n.oid = c.relnamespace
109
- where n.nspname = 'public'
110
- and c.relkind = 'r' -- tabelas regulares
111
- and c.relname ~ '_[0-9a-f]{8}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{12}$'
112
- ),
113
- per_org as (
114
- select
115
- org_id,
116
- sum(bytes) / (1024.0^3) as storage_gb
117
- from per_partition
118
- group by 1
119
- ),
120
- stats as (
121
- select percentile_cont(0.5) within group (order by storage_gb) as p50 from per_org
122
- )
123
- select
124
- per_org.org_id,
125
- round(per_org.storage_gb::numeric, 3) as storage_gb,
126
- round(stats.p50::numeric, 3) as p50_gb,
127
- round((per_org.storage_gb / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
128
- case
129
- when per_org.storage_gb > 10 * stats.p50 then 'CRITICAL'
130
- when per_org.storage_gb > 3 * stats.p50 then 'WARN'
131
- else 'OK'
132
- end as severity
133
- from per_org cross join stats
134
- order by storage_gb desc;
135
- ```
136
-
137
- #### Métrica 3 — Ratio conn slots via `pg_stat_activity`
138
-
139
- ```sql
140
- -- Pré-requisito: app seta application_name com org context, ex: 'app:org=<uuid>:edge=lead-create'
141
- -- Convenção canônica documentada em b2b-saas-architecture
142
- create or replace view private.hot_tenant_conn_slots as
143
- with per_org as (
144
- select
145
- -- Extrai uuid do application_name após 'org='
146
- (regexp_match(application_name, 'org=([0-9a-f-]{36})'))[1]::uuid as org_id,
147
- count(*) as active_slots
148
- from pg_stat_activity
149
- where state = 'active'
150
- and application_name ~ 'org=[0-9a-f-]{36}'
151
- group by 1
152
- ),
153
- stats as (
154
- select percentile_cont(0.5) within group (order by active_slots) as p50 from per_org
155
- )
156
- select
157
- per_org.org_id,
158
- per_org.active_slots,
159
- stats.p50,
160
- round((per_org.active_slots::numeric / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
161
- case
162
- when per_org.active_slots > 10 * stats.p50 then 'CRITICAL'
163
- when per_org.active_slots > 3 * stats.p50 then 'WARN'
164
- else 'OK'
165
- end as severity
166
- from per_org cross join stats
167
- order by ratio_vs_p50 desc nulls last;
168
- ```
169
-
170
- **Hot tenant é confirmado quando ≥ 2 das 3 métricas estão em WARN+ simultaneamente** — uma só métrica sozinha pode ser falso positivo (batch job, importação, migração). Triangulação reduz noise.
171
-
172
- ### REQ TENANT-02 — 5 estratégias de mitigação (tabela canônica)
173
-
174
- | # | Estratégia | Quando usar | Tradeoff principal | Config / SQL exemplo |
175
- |---|---|---|---|---|
176
- | 1 | **Rate limit por tenant** | Picos imprevisíveis de write/read em hot tenant que prejudicam P95 dos demais | Impacto UX no tenant target — usuário vê HTTP 429; precisa coordenar com customer success | RLS reject + `pg_cron` throttle counter (abaixo) |
177
- | 2 | **Pool conexão isolado (Supavisor multi-pool)** | Conn starvation — hot tenant esgota slots na pool compartilhada | Custo Supavisor multi-pool (Pro+) + complexidade de routing | Supavisor config `[pools.org_<uuid>]` |
178
- | 3 | **Read replica dedicada** | Tenant read-heavy (dashboards, exports) que não precisa de write strong consistency | Custo Supabase Pro+ + lag replicação aceitável (centenas ms) | Supavisor `read.*` routing + `application_name` hint |
179
- | 4 | **Desnormalização (MV per-tenant)** | Query repetitiva pesada (agregações, joins 5+ tabelas) que rodam 100× / hora p/ mesmo tenant | Refresh complexity + staleness window aceitável (5-15min) | `CREATE MATERIALIZED VIEW ... REFRESH CONCURRENTLY` + `pg_cron` |
180
- | 5 | **Request shaping (pgmq priority)** | Picos previsíveis batch (relatório fim-de-mês, importação) — work é assíncrono | Complexidade fila + worker; latency aumenta para hot tenant | `pgmq` priority queue + worker que drena LOW após HIGH |
181
-
182
- #### Estratégia 1 — Rate limit por tenant (exemplo)
183
-
184
- ```sql
185
- -- Tabela counter: bucket por org × minuto
186
- create table private.tenant_rate_limit_buckets (
187
- org_id uuid not null,
188
- bucket_minute timestamptz not null,
189
- request_count int not null default 0,
190
- primary key (org_id, bucket_minute)
191
- );
192
-
193
- -- Função: incrementa counter e retorna se excedeu limite
194
- create or replace function private.check_tenant_rate_limit(
195
- p_org_id uuid,
196
- p_limit_per_min int default 1000
197
- )
198
- returns boolean
199
- language plpgsql
200
- security definer
201
- set search_path = ''
202
- as $$
203
- declare
204
- v_count int;
205
- v_bucket timestamptz;
206
- begin
207
- v_bucket := date_trunc('minute', now());
208
- insert into private.tenant_rate_limit_buckets (org_id, bucket_minute, request_count)
209
- values (p_org_id, v_bucket, 1)
210
- on conflict (org_id, bucket_minute)
211
- do update set request_count = tenant_rate_limit_buckets.request_count + 1
212
- returning request_count into v_count;
213
- return v_count <= p_limit_per_min;
214
- end;
215
- $$;
216
-
217
- -- Cleanup buckets > 1h (pg_cron)
218
- select cron.schedule('cleanup-rate-limit-buckets', '*/15 * * * *', $$
219
- delete from private.tenant_rate_limit_buckets
220
- where bucket_minute < now() - interval '1 hour';
221
- $$);
222
- ```
223
-
224
- #### Estratégia 4 — MV per-tenant (exemplo agregação leads)
225
-
226
- ```sql
227
- -- MV agregando métricas pesadas só para hot tenant
228
- -- (Para os demais tenants, query original direto na tabela ainda é rápida)
229
- create materialized view public.lead_metrics_org_<uuid_underscore> as
230
- select
231
- l.stage,
232
- count(*) as count,
233
- count(*) filter (where l.created_at > now() - interval '7 days') as last_7d
234
- from public.leads l
235
- where l.org_id = '<uuid>'
236
- group by l.stage;
237
-
238
- create unique index lead_metrics_org_<uuid_underscore>_stage_idx
239
- on public.lead_metrics_org_<uuid_underscore> (stage);
240
-
241
- -- Refresh concurrent a cada 10min
242
- select cron.schedule(
243
- 'refresh-lead-metrics-org-<uuid_underscore>',
244
- '*/10 * * * *',
245
- $$ refresh materialized view concurrently public.lead_metrics_org_<uuid_underscore>; $$
246
- );
247
- ```
248
-
249
- #### Estratégia 5 — Request shaping (pgmq priority)
250
-
251
- ```sql
252
- -- 2 filas: high (pequenos clientes) + low (hot tenant batch)
253
- select pgmq.create('exports_high');
254
- select pgmq.create('exports_low');
255
-
256
- -- Enqueue: hot tenant vai para low, demais para high
257
- create or replace function public.enqueue_export(p_org_id uuid, p_payload jsonb)
258
- returns bigint
259
- language plpgsql
260
- security invoker
261
- set search_path = ''
262
- as $$
263
- declare
264
- v_is_hot boolean;
265
- begin
266
- -- Lookup hot tenant (refresh por job separado)
267
- select exists (select 1 from private.hot_tenant_registry where org_id = p_org_id and active)
268
- into v_is_hot;
269
- if v_is_hot then
270
- return (select msg_id from pgmq.send('exports_low', p_payload));
271
- else
272
- return (select msg_id from pgmq.send('exports_high', p_payload));
273
- end if;
274
- end;
275
- $$;
276
-
277
- -- Worker: drena high primeiro, low só quando high vazio
278
- -- (implementação Edge Function com Deno cron)
279
- ```
280
-
281
- ### REQ TENANT-03 — Particionamento range vs hash para `tenant_id`
282
-
283
- #### Decision tree
284
-
285
- ```
286
- Tabela > 50k rows/tenant OU > 5M rows total?
287
- ├── Não → SEM particionamento (overhead > benefit). Use partial indexes.
288
- └── Sim → particionar
289
- ├── Workload uniforme cross-tenant (P95 ratio < 2× P50)?
290
- │ ├── Sim → HASH (org_id) com 16-64 partições fixas
291
- │ └── Não → continuar abaixo
292
- └── Hot tenants conhecidos a priori (anchor tenant onboarded com SLA)?
293
- ├── Sim → RANGE (org_id) com partição manual para cada hot
294
- └── Não → HASH (default seguro) + monitor com REQ TENANT-01
295
- ```
296
-
297
- #### Hash partitioning — workload uniforme
298
-
299
- ```sql
300
- -- Tabela particionada por HASH em 16 partições (typical sweet spot Postgres 16+)
301
- create table public.events (
302
- id uuid not null,
303
- org_id uuid not null,
304
- event_type text not null,
305
- payload jsonb,
306
- created_at timestamptz not null default now(),
307
- primary key (org_id, id)
308
- ) partition by hash (org_id);
309
-
310
- -- Cria 16 partições — Postgres distribui via hash modulo 16
311
- do $$
312
- declare
313
- i int;
314
- begin
315
- for i in 0..15 loop
316
- execute format(
317
- 'create table public.events_p%s partition of public.events for values with (modulus 16, remainder %s)',
318
- lpad(i::text, 2, '0'), i
319
- );
320
- end loop;
321
- end $$;
322
-
323
- -- Index local em cada partição (document-partitioned — REQ TENANT-04)
324
- create index events_org_created_idx on public.events (org_id, created_at desc);
325
- ```
326
-
327
- **Por que 16 partições:** sweet spot empírico Postgres 16+ — partição management overhead negligível, paralelização de scans efetiva. Acima de 64 partições, planner começa a sofrer (citação DDIA p.202 — "each partition also has management overhead").
328
-
329
- #### Range partitioning — anchor tenant conhecido
330
-
331
- ```sql
332
- -- Tabela particionada por RANGE — partição dedicada para anchor tenant + default p/ os demais
333
- create table public.audit_logs (
334
- id uuid not null,
335
- org_id uuid not null,
336
- event_type text not null,
337
- actor_id uuid,
338
- payload jsonb,
339
- created_at timestamptz not null default now(),
340
- primary key (org_id, id)
341
- ) partition by range (org_id);
342
-
343
- -- Partição dedicada para anchor tenant (uuid conhecido)
344
- create table public.audit_logs_anchor_acme
345
- partition of public.audit_logs
346
- for values from ('11111111-1111-1111-1111-111111111111')
347
- to ('11111111-1111-1111-1111-111111111112');
348
-
349
- -- Partição default para todos os demais — rebalancear manualmente quando outro tenant virar hot
350
- create table public.audit_logs_default
351
- partition of public.audit_logs
352
- default;
353
-
354
- -- Índice local em cada partição
355
- create index audit_logs_anchor_acme_created_idx on public.audit_logs_anchor_acme (created_at desc);
356
- create index audit_logs_default_org_created_idx on public.audit_logs_default (org_id, created_at desc);
357
- ```
358
-
359
- **Vantagem range para anchor:** isola I/O do anchor tenant. Bloat/vacuum/analyze de outras orgs não bloqueia o anchor. Permite tablespace dedicado em disco SSD separado.
360
-
361
- **Risco range:** se outro tenant escalar inesperadamente, partição default fica skewed. Mitigação: REQ TENANT-01 monitor + script de migração para nova partição range.
362
-
363
- ### REQ TENANT-04 — Índices secundários document-partitioned vs term-partitioned
364
-
365
- DDIA p.197-200 distingue duas estratégias para índices secundários em tabelas particionadas. Aplicado a queries cross-tenant em views super-admin (caso canônico em B2B SaaS):
366
-
367
- | Aspecto | Document-partitioned (local) | Term-partitioned (global) |
368
- |---|---|---|
369
- | **Topologia** | 1 índice por partição (default Postgres) | 1 índice global cobrindo todas as partições (não-default Postgres — exige extensão pg_partman ou abordagem manual) |
370
- | **Write cost** | Barato — 1 partição afetada | Caro — N partições do índice afetadas + lock cross-partição |
371
- | **Read cost (single tenant)** | O(log n) na partição alvo | O(log n) no índice global |
372
- | **Read cost (cross-tenant super-admin)** | **scatter-gather** — todas as partições consultadas em paralelo | O(log n) — 1 lookup direto |
373
- | **Aplicação canônica** | RLS queries normais (filter por `org_id` → 1 partição) | Super-admin views que listam todas orgs por critério (ex: "todas as leads created_at > X") |
374
-
375
- #### Recomendação default: document-partitioned
376
-
377
- ```sql
378
- -- Index local em cada partição da tabela events (REQ TENANT-03)
379
- -- Postgres cria automaticamente em cada partição quando criado na tabela parent
380
- create index events_event_type_idx on public.events (event_type);
381
-
382
- -- Verificar que cada partição tem o index
383
- select
384
- pi.indrelid::regclass as partition_name,
385
- pi.indexrelid::regclass as index_name
386
- from pg_inherits inh
387
- join pg_index pi on pi.indrelid = inh.inhrelid
388
- where inh.inhparent = 'public.events'::regclass;
389
- ```
390
-
391
- **Query super-admin sobre `event_type` faz scatter-gather** — Postgres pruner não consegue eliminar partições (filter não inclui `org_id`). Custo: tail latency amplification (DDIA p.198) — query é tão lenta quanto a partição mais lenta. Aceitável para super-admin (queries raras, async, não user-facing).
392
-
393
- #### Term-partitioned (quando query path é crítico)
394
-
395
- Postgres não suporta nativamente índice global em tabela particionada. Opções:
396
-
397
- 1. **Tabela auxiliar de lookup** — manualmente mantida via trigger:
398
-
399
- ```sql
400
- -- Lookup table cross-tenant: term → (org_id, event_id)
401
- -- Mantida via trigger nas partições filhas
402
- create table private.events_event_type_global_idx (
403
- event_type text not null,
404
- org_id uuid not null,
405
- event_id uuid not null,
406
- created_at timestamptz not null,
407
- primary key (event_type, created_at desc, org_id, event_id)
408
- );
409
-
410
- create or replace function private.events_sync_global_idx()
411
- returns trigger
412
- language plpgsql
413
- security definer
414
- set search_path = ''
415
- as $$
416
- begin
417
- if (tg_op = 'INSERT') then
418
- insert into private.events_event_type_global_idx
419
- (event_type, org_id, event_id, created_at)
420
- values (new.event_type, new.org_id, new.id, new.created_at);
421
- elsif (tg_op = 'DELETE') then
422
- delete from private.events_event_type_global_idx
423
- where event_type = old.event_type and event_id = old.id;
424
- end if;
425
- return null;
426
- end;
427
- $$;
428
-
429
- -- Trigger replicado em cada partição (script bash gera a partir de pg_inherits)
430
- -- Custo: 2× write (tabela + lookup) + lock cross-partição quando lookup é atualizado
431
- ```
432
-
433
- 2. **Aceitar staleness via job batch** — DDIA p.200 nota que DynamoDB GSI tem propagação assíncrona "within a fraction of a second". Mesmo trade-off vale aqui:
434
-
435
- ```sql
436
- -- Refresh global index via pg_cron a cada 30s
437
- select cron.schedule('refresh-events-global-idx', '*/30 * * * * *', $$
438
- insert into private.events_event_type_global_idx
439
- (event_type, org_id, event_id, created_at)
440
- select event_type, org_id, id, created_at
441
- from public.events
442
- where created_at > coalesce((select max(created_at) from private.events_event_type_global_idx), 'epoch')
443
- on conflict do nothing;
444
- $$);
445
- ```
446
-
447
- **Recomendação canônica:** começar com document-partitioned. Migrar para term-partitioned **somente** quando query path super-admin específica for medida em > 5s P95 e não-async-tolerant.
448
-
449
- ### REQ TENANT-05 — Rebalanceamento sem downtime (4 passos)
450
-
451
- DDIA p.201-204 documenta que rebalancing tem 3 requisitos não-negociáveis: load fair pós-rebalance, sem downtime durante rebalance, mover só o necessário. Aplicado a Postgres + Supavisor:
452
-
453
- #### Passo 1 — Detectar tenant alvo via thresholds (REQ TENANT-01)
454
-
455
- Confirmado quando ≥ 2 das 3 métricas em CRITICAL por > 7 dias consecutivos. Decisão: humano (DBA + customer success) revisa antes de prosseguir. **NÃO automatizar.**
456
-
457
- #### Passo 2 — Dump do tenant para schema isolado
458
-
459
- ```bash
460
- # Pré-requisito: app está em modo read-only para o hot tenant durante 30min de janela de manutenção
461
- # (controlado por feature flag — coordenado com customer success)
462
-
463
- # Dump apenas tabelas do tenant (assumindo convenção partition naming)
464
- pg_dump \
465
- --schema=public \
466
- --table='*tenant_<uuid_underscore>*' \
467
- --table='public.events_<uuid_underscore>' \
468
- --table='public.audit_logs_<uuid_underscore>' \
469
- --no-owner \
470
- --no-acl \
471
- --file=/tmp/tenant_<uuid>_dump.sql \
472
- postgresql://postgres:<password>@db.<source_project_ref>.supabase.co:5432/postgres
473
-
474
- # Restaurar em nova instância Supabase dedicada (criada previamente)
475
- psql \
476
- postgresql://postgres:<password>@db.<dedicated_project_ref>.supabase.co:5432/postgres \
477
- < /tmp/tenant_<uuid>_dump.sql
478
-
479
- # Validar row count match
480
- psql <source> -c "select count(*) from public.events where org_id = '<uuid>';"
481
- psql <dedicated> -c "select count(*) from public.events;"
482
- ```
483
-
484
- #### Passo 3 — Supavisor redirect via routing config
485
-
486
- ```toml
487
- # supavisor.toml (ou config UI Supabase Dashboard)
488
- # Routing rule: requests com header X-Org-Id=<uuid> vão para instância dedicada
489
- [[routes]]
490
- match.header = "X-Org-Id"
491
- match.value = "<uuid>"
492
- target = "dedicated_<uuid>"
493
- priority = 100
494
-
495
- [pools.dedicated_<uuid>]
496
- host = "db.<dedicated_project_ref>.supabase.co"
497
- port = 5432
498
- database = "postgres"
499
- mode = "transaction"
500
- pool_size = 50
501
-
502
- # Default route para os demais tenants (instância original)
503
- [[routes]]
504
- match.default = true
505
- target = "shared"
506
- priority = 1
507
- ```
508
-
509
- ```typescript
510
- // App: setar header X-Org-Id em toda request
511
- // Supabase JS client custom header (versão >= 2.x)
512
- const supabase = createClient(url, anon_key, {
513
- global: {
514
- headers: { 'X-Org-Id': activeOrgId }
515
- }
516
- })
517
- ```
518
-
519
- **Após reload Supavisor (zero downtime — connections drain gracefully), tráfego do tenant alvo vai para instância dedicada. Demais tenants seguem na instância original.**
520
-
521
- #### Passo 4 — Cleanup conservador (após 7d sem queries)
522
-
523
- ```sql
524
- -- Verificar que nenhuma query tocou as partições antigas nos últimos 7d
525
- select
526
- schemaname, relname,
527
- last_seq_scan,
528
- last_idx_scan,
529
- greatest(coalesce(last_seq_scan, 'epoch'::timestamptz),
530
- coalesce(last_idx_scan, 'epoch'::timestamptz)) as last_access
531
- from pg_stat_user_tables
532
- where relname like '%<uuid_underscore>%'
533
- order by last_access;
534
- -- Esperado: last_access < now() - interval '7 days' para todas
535
-
536
- -- Apenas após confirmação manual humana, dropar
537
- begin;
538
- drop table if exists public.events_<uuid_underscore> cascade;
539
- drop table if exists public.audit_logs_<uuid_underscore> cascade;
540
- -- ... outras tabelas particionadas do tenant
541
- commit;
542
- ```
543
-
544
- **Por que 7d:** janela de defesa contra rollback emergencial. Se a instância dedicada falhar por bug não detectado em customer testing, voltar tráfego para instância original em < 5min via reverter Supavisor config — só funciona se dados antigos ainda existem.
545
-
546
- ## Anti-patterns
547
-
548
- ### Anti-pattern 1: Mitigar antes de medir (sem baseline 30d)
549
-
550
- **Errado:** "Cliente reclamou de lentidão — vamos criar MV per-tenant para ele agora."
551
-
552
- **Por quê:** sem baseline 30d das 3 métricas (REQ TENANT-01), não dá pra distinguir hot tenant real de pico transitório (importação CSV grande, batch fim-de-mês). Mitigação prematura adiciona MV refresh overhead permanente para uma situação possivelmente pontual.
553
-
554
- **Certo:** coletar 30d de baseline, identificar via REQ TENANT-01, confirmar com ≥ 2 das 3 métricas em WARN+ por > 7d. Só então aplicar mitigação.
555
-
556
- ### Anti-pattern 2: Particionar tabela com poucos rows
557
-
558
- **Errado:**
559
- ```sql
560
- -- 5 tenants, 200 rows/tenant
561
- create table public.events (...) partition by hash (org_id);
562
- ```
563
-
564
- **Por quê:** overhead de partition pruning + planner trabalho > benefit. Cada query passa por partition routing, dump/restore mais lento, manutenção complexa. Premature optimization clássica — DDIA p.202 nota que "each partition also has management overhead".
565
-
566
- **Certo:** começar com tabela regular + index `(org_id, created_at desc)`. Particionar quando atingir threshold real (> 50k rows/tenant OU > 5M total).
567
-
568
- ### Anti-pattern 3: Term-partitioned como default
569
-
570
- **Errado:** criar lookup table global (term-partitioned) já no MVP "para evitar scatter-gather no futuro".
571
-
572
- **Por quê:** writes ficam 2× mais caros desde dia 1. Cross-partition lock complica. DDIA p.200 documenta que mesmo DynamoDB GSI (term-partitioned built-in) tem trade-off de propagation delay assíncrono. Você está pagando custo agora para benefício hipotético futuro.
573
-
574
- **Certo:** document-partitioned como default. Migrar para term-partitioned **somente** quando query path super-admin medir > 5s P95 e for user-facing crítico.
575
-
576
- ### Anti-pattern 4: Rebalancing automático
577
-
578
- **Errado:** script bash que detecta hot tenant via REQ TENANT-01 e automaticamente roda passos 2-3 do REQ TENANT-05.
579
-
580
- **Por quê:** DDIA p.204 documenta cascading failure clássica — node lento detectado como dead → rebalance automático → carga extra no resto do cluster → mais nodes ficam lentos → mais rebalance → cascade. Em B2B SaaS, equivalente: importação CSV grande detectada como hot → rebalance triggered → aplicação volta-volta no meio de transação user-facing → erros 500 em produção.
581
-
582
- **Certo:** detecção automática gera **alerta** (Slack/PagerDuty). Decisão de rebalance é humana (DBA + customer success), executada em janela de manutenção pré-comunicada.
583
-
584
- ### Anti-pattern 5: Cleanup imediato após move (sem 7d)
585
-
586
- **Errado:**
587
- ```sql
588
- -- Logo após Supavisor reroute (REQ TENANT-05 passo 3)
589
- drop schema tenant_<uuid> cascade;
590
- ```
591
-
592
- **Por quê:** se instância dedicada tiver bug não detectado (RLS quebrada, schema diverge, performance pior), você não consegue rollback. Customer fica fora do ar até nova restore from backup (RTO horas).
593
-
594
- **Certo:** 7d de monitoring ativo (`pg_stat_user_tables.last_seq_scan`/`last_idx_scan` confirmados zero) antes do drop. Custo: 7d de storage duplicado (negligível vs custo de outage).
595
-
596
- ## Ver também
597
-
598
- - [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) — glossário compartilhado da Suíte DDIA Foundations v1.22 (define `hot spot`, `scatter-gather`, `consistent hashing`, `key range partitioning`, etc.)
599
- - [`../multi-tenant-performance-scaling/SKILL.md`](../multi-tenant-performance-scaling/SKILL.md) — Supavisor pooling, partial indexes, helper functions STABLE (skill irmã v1.21 — base de scaling antes de mitigação de hot tenant)
600
- - [`../supabase-postgres-style/SKILL.md`](../supabase-postgres-style/SKILL.md) — style guide SQL canônico (snake_case, schema-qualified, `private.*` para helpers)
601
- - [`../multi-tenant-rls-hierarchy/SKILL.md`](../multi-tenant-rls-hierarchy/SKILL.md) — RLS hierarchical policies que coexistem com partições (RLS aplicada na tabela parent propaga para todas as partições)
602
- - [`../super-admin-platform-pattern/SKILL.md`](../super-admin-platform-pattern/SKILL.md) — cross-tenant views super-admin (caso canônico para REQ TENANT-04 term-partitioned trade-off)
603
- - DDIA Ch 6 (Designing Data-Intensive Applications, Martin Kleppmann) — Partitioning. Justin Bieber tenant: p.196 nota [13]. Hash vs range: p.194-196. Secondary indexes: p.197-200. Rebalancing: p.201-204.
604
- - [Postgres Declarative Partitioning Docs](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE)
605
- - [Supavisor Multi-Pool Docs](https://supabase.com/docs/guides/database/connecting-to-postgres#supavisor)
1
+ ---
2
+ name: tenant-quente-mitigacao
3
+ description: Use ao escalar Postgres multi-tenant em Supabase quando 1 tenant consome >>> que outros (problema "Justin Bieber tenant" do DDIA Ch 6)…
4
+ ---
5
+
6
+ # Tenant Quente — Mitigação (DDIA Ch 6 aplicado a Postgres + Supabase)
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando há **suspeita ou evidência de skewed workload em B2B SaaS multi-tenant** — i.e. um tenant (ou pequeno conjunto) consome desproporcionalmente recursos vs P50 dos demais. DDIA Ch 6 chama isso de **hot spot**, e o anchor narrativo canônico é o "Justin Bieber tenant" — referência ao caso Twitter onde 3% dos servidores ficaram dedicados a 1 celebrity user (DDIA p.196 nota [13]). Em B2B SaaS, o equivalente é **1 cliente enterprise** (ou anchor tenant) que escala 10× mais rápido que o restante da base.
11
+
12
+ Trigger phrases:
13
+
14
+ - "tenant Justin Bieber", "hot tenant", "skewed multi-tenant"
15
+ - "1 cliente consumindo a base inteira", "tenant dominante", "anchor tenant"
16
+ - "particionamento por tenant", "PARTITION BY HASH/RANGE org_id"
17
+ - "scatter-gather Postgres super-admin"
18
+ - "rebalancear tenant sem downtime", "mover tenant para schema dedicado"
19
+ - "MV per-tenant pesada", "queue priority por tenant"
20
+
21
+ Esta skill é consumida por `multi-tenant-isolation-auditor` (v1.21) ao detectar tabelas suspeitas de skew, por `omm-auditor` (v1.10) ao avaliar capacidade de escala, e por `b2b-saas-architect` (v1.21) ao desenhar schema de novo cliente enterprise grande.
22
+
23
+ ## Regras absolutas
24
+
25
+ **REGRA #1 (medir antes de mitigar):** **NUNCA** aplicar mitigação sem coletar baseline 30d das 3 métricas canônicas (REQ TENANT-01). Mitigação prematura = otimização cega. Threshold canônico: WARN >3× P50, CRITICAL >10× P50.
26
+
27
+ **REGRA #2 (default document-partitioned):** Índices secundários em tabelas particionadas devem ser **document-partitioned (local)** por default. Term-partitioned (global) **só** em query path crítica onde scatter-gather é o gargalo medido.
28
+
29
+ **REGRA #3 (hash quando uniforme, range quando skewed conhecido):** Particionar por `HASH (org_id)` quando workload é uniforme cross-tenant. Particionar por `RANGE (org_id)` apenas quando hot tenants são **conhecidos a priori** (anchor tenant enterprise onboarded com SLA dedicado).
30
+
31
+ **REGRA #4 (rebalanceamento manual, nunca automático):** Mover tenant para schema/instância dedicada **NUNCA** automaticamente. Sempre humano-no-loop com janela de manutenção comunicada — DDIA p.204 ("Operations: automatic or manual rebalancing") documenta o risco de cascading failure quando rebalance auto reage a node lento.
32
+
33
+ **REGRA #5 (cleanup conservador):** Após mover tenant, **NUNCA** dropar schema/dados antigos antes de **7d sem queries** confirmados via `pg_stat_user_tables.last_seq_scan` + `last_idx_scan`. Defesa contra rollback emergencial.
34
+
35
+ ## Patterns canônicos
36
+
37
+ ### REQ TENANT-01 — Detecção do "tenant Justin Bieber"
38
+
39
+ Três métricas canônicas, todas com baseline 30d e threshold relativo ao P50 da base de tenants ativos:
40
+
41
+ #### Métrica 1 — Ratio queries/min via `pg_stat_statements`
42
+
43
+ ```sql
44
+ -- Pré-requisito: pg_stat_statements habilitado (Supabase: Settings → Database → Extensions)
45
+ -- Helper: extrai org_id do parameter da query (assume RLS sempre filtra por org_id literal/parameter)
46
+ create or replace function private.extract_org_id_from_query(p_query text)
47
+ returns uuid
48
+ language plpgsql
49
+ immutable
50
+ set search_path = ''
51
+ as $$
52
+ declare
53
+ m text[];
54
+ begin
55
+ -- Casa UUID em formato canônico no texto da query (parameter-bound)
56
+ m := regexp_match(p_query, '''([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})''');
57
+ if m is null then
58
+ return null;
59
+ end if;
60
+ return m[1]::uuid;
61
+ end;
62
+ $$;
63
+
64
+ -- View canônica: queries/min por org_id sobre janela 24h
65
+ create or replace view private.hot_tenant_query_rate as
66
+ with per_org as (
67
+ select
68
+ private.extract_org_id_from_query(query) as org_id,
69
+ sum(calls) / nullif(extract(epoch from (now() - stats_reset)) / 60, 0) as queries_per_min
70
+ from pg_stat_statements
71
+ where private.extract_org_id_from_query(query) is not null
72
+ group by 1
73
+ ),
74
+ stats as (
75
+ select
76
+ percentile_cont(0.5) within group (order by queries_per_min) as p50
77
+ from per_org
78
+ )
79
+ select
80
+ per_org.org_id,
81
+ per_org.queries_per_min,
82
+ stats.p50,
83
+ round((per_org.queries_per_min / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
84
+ case
85
+ when per_org.queries_per_min > 10 * stats.p50 then 'CRITICAL'
86
+ when per_org.queries_per_min > 3 * stats.p50 then 'WARN'
87
+ else 'OK'
88
+ end as severity
89
+ from per_org cross join stats
90
+ order by ratio_vs_p50 desc nulls last;
91
+ ```
92
+
93
+ #### Métrica 2 — Ratio storage GB via `pg_total_relation_size`
94
+
95
+ ```sql
96
+ -- View: storage por tenant agregando tabelas particionadas + tabelas não-particionadas
97
+ -- Assume convenção de naming partição: <tabela_base>_<org_id_underscore>
98
+ create or replace view private.hot_tenant_storage as
99
+ with per_partition as (
100
+ select
101
+ -- Extrai org_id do nome da partição (audit_logs_<uuid_underscore> -> uuid)
102
+ replace(
103
+ regexp_replace(c.relname, '^[a-z_]+_([0-9a-f_]{36})$', '\1'),
104
+ '_', '-'
105
+ )::uuid as org_id,
106
+ pg_total_relation_size(c.oid) as bytes
107
+ from pg_class c
108
+ join pg_namespace n on n.oid = c.relnamespace
109
+ where n.nspname = 'public'
110
+ and c.relkind = 'r' -- tabelas regulares
111
+ and c.relname ~ '_[0-9a-f]{8}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{12}$'
112
+ ),
113
+ per_org as (
114
+ select
115
+ org_id,
116
+ sum(bytes) / (1024.0^3) as storage_gb
117
+ from per_partition
118
+ group by 1
119
+ ),
120
+ stats as (
121
+ select percentile_cont(0.5) within group (order by storage_gb) as p50 from per_org
122
+ )
123
+ select
124
+ per_org.org_id,
125
+ round(per_org.storage_gb::numeric, 3) as storage_gb,
126
+ round(stats.p50::numeric, 3) as p50_gb,
127
+ round((per_org.storage_gb / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
128
+ case
129
+ when per_org.storage_gb > 10 * stats.p50 then 'CRITICAL'
130
+ when per_org.storage_gb > 3 * stats.p50 then 'WARN'
131
+ else 'OK'
132
+ end as severity
133
+ from per_org cross join stats
134
+ order by storage_gb desc;
135
+ ```
136
+
137
+ #### Métrica 3 — Ratio conn slots via `pg_stat_activity`
138
+
139
+ ```sql
140
+ -- Pré-requisito: app seta application_name com org context, ex: 'app:org=<uuid>:edge=lead-create'
141
+ -- Convenção canônica documentada em b2b-saas-architecture
142
+ create or replace view private.hot_tenant_conn_slots as
143
+ with per_org as (
144
+ select
145
+ -- Extrai uuid do application_name após 'org='
146
+ (regexp_match(application_name, 'org=([0-9a-f-]{36})'))[1]::uuid as org_id,
147
+ count(*) as active_slots
148
+ from pg_stat_activity
149
+ where state = 'active'
150
+ and application_name ~ 'org=[0-9a-f-]{36}'
151
+ group by 1
152
+ ),
153
+ stats as (
154
+ select percentile_cont(0.5) within group (order by active_slots) as p50 from per_org
155
+ )
156
+ select
157
+ per_org.org_id,
158
+ per_org.active_slots,
159
+ stats.p50,
160
+ round((per_org.active_slots::numeric / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
161
+ case
162
+ when per_org.active_slots > 10 * stats.p50 then 'CRITICAL'
163
+ when per_org.active_slots > 3 * stats.p50 then 'WARN'
164
+ else 'OK'
165
+ end as severity
166
+ from per_org cross join stats
167
+ order by ratio_vs_p50 desc nulls last;
168
+ ```
169
+
170
+ **Hot tenant é confirmado quando ≥ 2 das 3 métricas estão em WARN+ simultaneamente** — uma só métrica sozinha pode ser falso positivo (batch job, importação, migração). Triangulação reduz noise.
171
+
172
+ ### REQ TENANT-02 — 5 estratégias de mitigação (tabela canônica)
173
+
174
+ | # | Estratégia | Quando usar | Tradeoff principal | Config / SQL exemplo |
175
+ |---|---|---|---|---|
176
+ | 1 | **Rate limit por tenant** | Picos imprevisíveis de write/read em hot tenant que prejudicam P95 dos demais | Impacto UX no tenant target — usuário vê HTTP 429; precisa coordenar com customer success | RLS reject + `pg_cron` throttle counter (abaixo) |
177
+ | 2 | **Pool conexão isolado (Supavisor multi-pool)** | Conn starvation — hot tenant esgota slots na pool compartilhada | Custo Supavisor multi-pool (Pro+) + complexidade de routing | Supavisor config `[pools.org_<uuid>]` |
178
+ | 3 | **Read replica dedicada** | Tenant read-heavy (dashboards, exports) que não precisa de write strong consistency | Custo Supabase Pro+ + lag replicação aceitável (centenas ms) | Supavisor `read.*` routing + `application_name` hint |
179
+ | 4 | **Desnormalização (MV per-tenant)** | Query repetitiva pesada (agregações, joins 5+ tabelas) que rodam 100× / hora p/ mesmo tenant | Refresh complexity + staleness window aceitável (5-15min) | `CREATE MATERIALIZED VIEW ... REFRESH CONCURRENTLY` + `pg_cron` |
180
+ | 5 | **Request shaping (pgmq priority)** | Picos previsíveis batch (relatório fim-de-mês, importação) — work é assíncrono | Complexidade fila + worker; latency aumenta para hot tenant | `pgmq` priority queue + worker que drena LOW após HIGH |
181
+
182
+ #### Estratégia 1 — Rate limit por tenant (exemplo)
183
+
184
+ ```sql
185
+ -- Tabela counter: bucket por org × minuto
186
+ create table private.tenant_rate_limit_buckets (
187
+ org_id uuid not null,
188
+ bucket_minute timestamptz not null,
189
+ request_count int not null default 0,
190
+ primary key (org_id, bucket_minute)
191
+ );
192
+
193
+ -- Função: incrementa counter e retorna se excedeu limite
194
+ create or replace function private.check_tenant_rate_limit(
195
+ p_org_id uuid,
196
+ p_limit_per_min int default 1000
197
+ )
198
+ returns boolean
199
+ language plpgsql
200
+ security definer
201
+ set search_path = ''
202
+ as $$
203
+ declare
204
+ v_count int;
205
+ v_bucket timestamptz;
206
+ begin
207
+ v_bucket := date_trunc('minute', now());
208
+ insert into private.tenant_rate_limit_buckets (org_id, bucket_minute, request_count)
209
+ values (p_org_id, v_bucket, 1)
210
+ on conflict (org_id, bucket_minute)
211
+ do update set request_count = tenant_rate_limit_buckets.request_count + 1
212
+ returning request_count into v_count;
213
+ return v_count <= p_limit_per_min;
214
+ end;
215
+ $$;
216
+
217
+ -- Cleanup buckets > 1h (pg_cron)
218
+ select cron.schedule('cleanup-rate-limit-buckets', '*/15 * * * *', $$
219
+ delete from private.tenant_rate_limit_buckets
220
+ where bucket_minute < now() - interval '1 hour';
221
+ $$);
222
+ ```
223
+
224
+ #### Estratégia 4 — MV per-tenant (exemplo agregação leads)
225
+
226
+ ```sql
227
+ -- MV agregando métricas pesadas só para hot tenant
228
+ -- (Para os demais tenants, query original direto na tabela ainda é rápida)
229
+ create materialized view public.lead_metrics_org_<uuid_underscore> as
230
+ select
231
+ l.stage,
232
+ count(*) as count,
233
+ count(*) filter (where l.created_at > now() - interval '7 days') as last_7d
234
+ from public.leads l
235
+ where l.org_id = '<uuid>'
236
+ group by l.stage;
237
+
238
+ create unique index lead_metrics_org_<uuid_underscore>_stage_idx
239
+ on public.lead_metrics_org_<uuid_underscore> (stage);
240
+
241
+ -- Refresh concurrent a cada 10min
242
+ select cron.schedule(
243
+ 'refresh-lead-metrics-org-<uuid_underscore>',
244
+ '*/10 * * * *',
245
+ $$ refresh materialized view concurrently public.lead_metrics_org_<uuid_underscore>; $$
246
+ );
247
+ ```
248
+
249
+ #### Estratégia 5 — Request shaping (pgmq priority)
250
+
251
+ ```sql
252
+ -- 2 filas: high (pequenos clientes) + low (hot tenant batch)
253
+ select pgmq.create('exports_high');
254
+ select pgmq.create('exports_low');
255
+
256
+ -- Enqueue: hot tenant vai para low, demais para high
257
+ create or replace function public.enqueue_export(p_org_id uuid, p_payload jsonb)
258
+ returns bigint
259
+ language plpgsql
260
+ security invoker
261
+ set search_path = ''
262
+ as $$
263
+ declare
264
+ v_is_hot boolean;
265
+ begin
266
+ -- Lookup hot tenant (refresh por job separado)
267
+ select exists (select 1 from private.hot_tenant_registry where org_id = p_org_id and active)
268
+ into v_is_hot;
269
+ if v_is_hot then
270
+ return (select msg_id from pgmq.send('exports_low', p_payload));
271
+ else
272
+ return (select msg_id from pgmq.send('exports_high', p_payload));
273
+ end if;
274
+ end;
275
+ $$;
276
+
277
+ -- Worker: drena high primeiro, low só quando high vazio
278
+ -- (implementação Edge Function com Deno cron)
279
+ ```
280
+
281
+ ### REQ TENANT-03 — Particionamento range vs hash para `tenant_id`
282
+
283
+ #### Decision tree
284
+
285
+ ```
286
+ Tabela > 50k rows/tenant OU > 5M rows total?
287
+ ├── Não → SEM particionamento (overhead > benefit). Use partial indexes.
288
+ └── Sim → particionar
289
+ ├── Workload uniforme cross-tenant (P95 ratio < 2× P50)?
290
+ │ ├── Sim → HASH (org_id) com 16-64 partições fixas
291
+ │ └── Não → continuar abaixo
292
+ └── Hot tenants conhecidos a priori (anchor tenant onboarded com SLA)?
293
+ ├── Sim → RANGE (org_id) com partição manual para cada hot
294
+ └── Não → HASH (default seguro) + monitor com REQ TENANT-01
295
+ ```
296
+
297
+ #### Hash partitioning — workload uniforme
298
+
299
+ ```sql
300
+ -- Tabela particionada por HASH em 16 partições (typical sweet spot Postgres 16+)
301
+ create table public.events (
302
+ id uuid not null,
303
+ org_id uuid not null,
304
+ event_type text not null,
305
+ payload jsonb,
306
+ created_at timestamptz not null default now(),
307
+ primary key (org_id, id)
308
+ ) partition by hash (org_id);
309
+
310
+ -- Cria 16 partições — Postgres distribui via hash modulo 16
311
+ do $$
312
+ declare
313
+ i int;
314
+ begin
315
+ for i in 0..15 loop
316
+ execute format(
317
+ 'create table public.events_p%s partition of public.events for values with (modulus 16, remainder %s)',
318
+ lpad(i::text, 2, '0'), i
319
+ );
320
+ end loop;
321
+ end $$;
322
+
323
+ -- Index local em cada partição (document-partitioned — REQ TENANT-04)
324
+ create index events_org_created_idx on public.events (org_id, created_at desc);
325
+ ```
326
+
327
+ **Por que 16 partições:** sweet spot empírico Postgres 16+ — partição management overhead negligível, paralelização de scans efetiva. Acima de 64 partições, planner começa a sofrer (citação DDIA p.202 — "each partition also has management overhead").
328
+
329
+ #### Range partitioning — anchor tenant conhecido
330
+
331
+ ```sql
332
+ -- Tabela particionada por RANGE — partição dedicada para anchor tenant + default p/ os demais
333
+ create table public.audit_logs (
334
+ id uuid not null,
335
+ org_id uuid not null,
336
+ event_type text not null,
337
+ actor_id uuid,
338
+ payload jsonb,
339
+ created_at timestamptz not null default now(),
340
+ primary key (org_id, id)
341
+ ) partition by range (org_id);
342
+
343
+ -- Partição dedicada para anchor tenant (uuid conhecido)
344
+ create table public.audit_logs_anchor_acme
345
+ partition of public.audit_logs
346
+ for values from ('11111111-1111-1111-1111-111111111111')
347
+ to ('11111111-1111-1111-1111-111111111112');
348
+
349
+ -- Partição default para todos os demais — rebalancear manualmente quando outro tenant virar hot
350
+ create table public.audit_logs_default
351
+ partition of public.audit_logs
352
+ default;
353
+
354
+ -- Índice local em cada partição
355
+ create index audit_logs_anchor_acme_created_idx on public.audit_logs_anchor_acme (created_at desc);
356
+ create index audit_logs_default_org_created_idx on public.audit_logs_default (org_id, created_at desc);
357
+ ```
358
+
359
+ **Vantagem range para anchor:** isola I/O do anchor tenant. Bloat/vacuum/analyze de outras orgs não bloqueia o anchor. Permite tablespace dedicado em disco SSD separado.
360
+
361
+ **Risco range:** se outro tenant escalar inesperadamente, partição default fica skewed. Mitigação: REQ TENANT-01 monitor + script de migração para nova partição range.
362
+
363
+ ### REQ TENANT-04 — Índices secundários document-partitioned vs term-partitioned
364
+
365
+ DDIA p.197-200 distingue duas estratégias para índices secundários em tabelas particionadas. Aplicado a queries cross-tenant em views super-admin (caso canônico em B2B SaaS):
366
+
367
+ | Aspecto | Document-partitioned (local) | Term-partitioned (global) |
368
+ |---|---|---|
369
+ | **Topologia** | 1 índice por partição (default Postgres) | 1 índice global cobrindo todas as partições (não-default Postgres — exige extensão pg_partman ou abordagem manual) |
370
+ | **Write cost** | Barato — 1 partição afetada | Caro — N partições do índice afetadas + lock cross-partição |
371
+ | **Read cost (single tenant)** | O(log n) na partição alvo | O(log n) no índice global |
372
+ | **Read cost (cross-tenant super-admin)** | **scatter-gather** — todas as partições consultadas em paralelo | O(log n) — 1 lookup direto |
373
+ | **Aplicação canônica** | RLS queries normais (filter por `org_id` → 1 partição) | Super-admin views que listam todas orgs por critério (ex: "todas as leads created_at > X") |
374
+
375
+ #### Recomendação default: document-partitioned
376
+
377
+ ```sql
378
+ -- Index local em cada partição da tabela events (REQ TENANT-03)
379
+ -- Postgres cria automaticamente em cada partição quando criado na tabela parent
380
+ create index events_event_type_idx on public.events (event_type);
381
+
382
+ -- Verificar que cada partição tem o index
383
+ select
384
+ pi.indrelid::regclass as partition_name,
385
+ pi.indexrelid::regclass as index_name
386
+ from pg_inherits inh
387
+ join pg_index pi on pi.indrelid = inh.inhrelid
388
+ where inh.inhparent = 'public.events'::regclass;
389
+ ```
390
+
391
+ **Query super-admin sobre `event_type` faz scatter-gather** — Postgres pruner não consegue eliminar partições (filter não inclui `org_id`). Custo: tail latency amplification (DDIA p.198) — query é tão lenta quanto a partição mais lenta. Aceitável para super-admin (queries raras, async, não user-facing).
392
+
393
+ #### Term-partitioned (quando query path é crítico)
394
+
395
+ Postgres não suporta nativamente índice global em tabela particionada. Opções:
396
+
397
+ 1. **Tabela auxiliar de lookup** — manualmente mantida via trigger:
398
+
399
+ ```sql
400
+ -- Lookup table cross-tenant: term → (org_id, event_id)
401
+ -- Mantida via trigger nas partições filhas
402
+ create table private.events_event_type_global_idx (
403
+ event_type text not null,
404
+ org_id uuid not null,
405
+ event_id uuid not null,
406
+ created_at timestamptz not null,
407
+ primary key (event_type, created_at desc, org_id, event_id)
408
+ );
409
+
410
+ create or replace function private.events_sync_global_idx()
411
+ returns trigger
412
+ language plpgsql
413
+ security definer
414
+ set search_path = ''
415
+ as $$
416
+ begin
417
+ if (tg_op = 'INSERT') then
418
+ insert into private.events_event_type_global_idx
419
+ (event_type, org_id, event_id, created_at)
420
+ values (new.event_type, new.org_id, new.id, new.created_at);
421
+ elsif (tg_op = 'DELETE') then
422
+ delete from private.events_event_type_global_idx
423
+ where event_type = old.event_type and event_id = old.id;
424
+ end if;
425
+ return null;
426
+ end;
427
+ $$;
428
+
429
+ -- Trigger replicado em cada partição (script bash gera a partir de pg_inherits)
430
+ -- Custo: 2× write (tabela + lookup) + lock cross-partição quando lookup é atualizado
431
+ ```
432
+
433
+ 2. **Aceitar staleness via job batch** — DDIA p.200 nota que DynamoDB GSI tem propagação assíncrona "within a fraction of a second". Mesmo trade-off vale aqui:
434
+
435
+ ```sql
436
+ -- Refresh global index via pg_cron a cada 30s
437
+ select cron.schedule('refresh-events-global-idx', '*/30 * * * * *', $$
438
+ insert into private.events_event_type_global_idx
439
+ (event_type, org_id, event_id, created_at)
440
+ select event_type, org_id, id, created_at
441
+ from public.events
442
+ where created_at > coalesce((select max(created_at) from private.events_event_type_global_idx), 'epoch')
443
+ on conflict do nothing;
444
+ $$);
445
+ ```
446
+
447
+ **Recomendação canônica:** começar com document-partitioned. Migrar para term-partitioned **somente** quando query path super-admin específica for medida em > 5s P95 e não-async-tolerant.
448
+
449
+ ### REQ TENANT-05 — Rebalanceamento sem downtime (4 passos)
450
+
451
+ DDIA p.201-204 documenta que rebalancing tem 3 requisitos não-negociáveis: load fair pós-rebalance, sem downtime durante rebalance, mover só o necessário. Aplicado a Postgres + Supavisor:
452
+
453
+ #### Passo 1 — Detectar tenant alvo via thresholds (REQ TENANT-01)
454
+
455
+ Confirmado quando ≥ 2 das 3 métricas em CRITICAL por > 7 dias consecutivos. Decisão: humano (DBA + customer success) revisa antes de prosseguir. **NÃO automatizar.**
456
+
457
+ #### Passo 2 — Dump do tenant para schema isolado
458
+
459
+ ```bash
460
+ # Pré-requisito: app está em modo read-only para o hot tenant durante 30min de janela de manutenção
461
+ # (controlado por feature flag — coordenado com customer success)
462
+
463
+ # Dump apenas tabelas do tenant (assumindo convenção partition naming)
464
+ pg_dump \
465
+ --schema=public \
466
+ --table='*tenant_<uuid_underscore>*' \
467
+ --table='public.events_<uuid_underscore>' \
468
+ --table='public.audit_logs_<uuid_underscore>' \
469
+ --no-owner \
470
+ --no-acl \
471
+ --file=/tmp/tenant_<uuid>_dump.sql \
472
+ postgresql://postgres:<password>@db.<source_project_ref>.supabase.co:5432/postgres
473
+
474
+ # Restaurar em nova instância Supabase dedicada (criada previamente)
475
+ psql \
476
+ postgresql://postgres:<password>@db.<dedicated_project_ref>.supabase.co:5432/postgres \
477
+ < /tmp/tenant_<uuid>_dump.sql
478
+
479
+ # Validar row count match
480
+ psql <source> -c "select count(*) from public.events where org_id = '<uuid>';"
481
+ psql <dedicated> -c "select count(*) from public.events;"
482
+ ```
483
+
484
+ #### Passo 3 — Supavisor redirect via routing config
485
+
486
+ ```toml
487
+ # supavisor.toml (ou config UI Supabase Dashboard)
488
+ # Routing rule: requests com header X-Org-Id=<uuid> vão para instância dedicada
489
+ [[routes]]
490
+ match.header = "X-Org-Id"
491
+ match.value = "<uuid>"
492
+ target = "dedicated_<uuid>"
493
+ priority = 100
494
+
495
+ [pools.dedicated_<uuid>]
496
+ host = "db.<dedicated_project_ref>.supabase.co"
497
+ port = 5432
498
+ database = "postgres"
499
+ mode = "transaction"
500
+ pool_size = 50
501
+
502
+ # Default route para os demais tenants (instância original)
503
+ [[routes]]
504
+ match.default = true
505
+ target = "shared"
506
+ priority = 1
507
+ ```
508
+
509
+ ```typescript
510
+ // App: setar header X-Org-Id em toda request
511
+ // Supabase JS client custom header (versão >= 2.x)
512
+ const supabase = createClient(url, anon_key, {
513
+ global: {
514
+ headers: { 'X-Org-Id': activeOrgId }
515
+ }
516
+ })
517
+ ```
518
+
519
+ **Após reload Supavisor (zero downtime — connections drain gracefully), tráfego do tenant alvo vai para instância dedicada. Demais tenants seguem na instância original.**
520
+
521
+ #### Passo 4 — Cleanup conservador (após 7d sem queries)
522
+
523
+ ```sql
524
+ -- Verificar que nenhuma query tocou as partições antigas nos últimos 7d
525
+ select
526
+ schemaname, relname,
527
+ last_seq_scan,
528
+ last_idx_scan,
529
+ greatest(coalesce(last_seq_scan, 'epoch'::timestamptz),
530
+ coalesce(last_idx_scan, 'epoch'::timestamptz)) as last_access
531
+ from pg_stat_user_tables
532
+ where relname like '%<uuid_underscore>%'
533
+ order by last_access;
534
+ -- Esperado: last_access < now() - interval '7 days' para todas
535
+
536
+ -- Apenas após confirmação manual humana, dropar
537
+ begin;
538
+ drop table if exists public.events_<uuid_underscore> cascade;
539
+ drop table if exists public.audit_logs_<uuid_underscore> cascade;
540
+ -- ... outras tabelas particionadas do tenant
541
+ commit;
542
+ ```
543
+
544
+ **Por que 7d:** janela de defesa contra rollback emergencial. Se a instância dedicada falhar por bug não detectado em customer testing, voltar tráfego para instância original em < 5min via reverter Supavisor config — só funciona se dados antigos ainda existem.
545
+
546
+ ## Anti-patterns
547
+
548
+ ### Anti-pattern 1: Mitigar antes de medir (sem baseline 30d)
549
+
550
+ **Errado:** "Cliente reclamou de lentidão — vamos criar MV per-tenant para ele agora."
551
+
552
+ **Por quê:** sem baseline 30d das 3 métricas (REQ TENANT-01), não dá pra distinguir hot tenant real de pico transitório (importação CSV grande, batch fim-de-mês). Mitigação prematura adiciona MV refresh overhead permanente para uma situação possivelmente pontual.
553
+
554
+ **Certo:** coletar 30d de baseline, identificar via REQ TENANT-01, confirmar com ≥ 2 das 3 métricas em WARN+ por > 7d. Só então aplicar mitigação.
555
+
556
+ ### Anti-pattern 2: Particionar tabela com poucos rows
557
+
558
+ **Errado:**
559
+ ```sql
560
+ -- 5 tenants, 200 rows/tenant
561
+ create table public.events (...) partition by hash (org_id);
562
+ ```
563
+
564
+ **Por quê:** overhead de partition pruning + planner trabalho > benefit. Cada query passa por partition routing, dump/restore mais lento, manutenção complexa. Premature optimization clássica — DDIA p.202 nota que "each partition also has management overhead".
565
+
566
+ **Certo:** começar com tabela regular + index `(org_id, created_at desc)`. Particionar quando atingir threshold real (> 50k rows/tenant OU > 5M total).
567
+
568
+ ### Anti-pattern 3: Term-partitioned como default
569
+
570
+ **Errado:** criar lookup table global (term-partitioned) já no MVP "para evitar scatter-gather no futuro".
571
+
572
+ **Por quê:** writes ficam 2× mais caros desde dia 1. Cross-partition lock complica. DDIA p.200 documenta que mesmo DynamoDB GSI (term-partitioned built-in) tem trade-off de propagation delay assíncrono. Você está pagando custo agora para benefício hipotético futuro.
573
+
574
+ **Certo:** document-partitioned como default. Migrar para term-partitioned **somente** quando query path super-admin medir > 5s P95 e for user-facing crítico.
575
+
576
+ ### Anti-pattern 4: Rebalancing automático
577
+
578
+ **Errado:** script bash que detecta hot tenant via REQ TENANT-01 e automaticamente roda passos 2-3 do REQ TENANT-05.
579
+
580
+ **Por quê:** DDIA p.204 documenta cascading failure clássica — node lento detectado como dead → rebalance automático → carga extra no resto do cluster → mais nodes ficam lentos → mais rebalance → cascade. Em B2B SaaS, equivalente: importação CSV grande detectada como hot → rebalance triggered → aplicação volta-volta no meio de transação user-facing → erros 500 em produção.
581
+
582
+ **Certo:** detecção automática gera **alerta** (Slack/PagerDuty). Decisão de rebalance é humana (DBA + customer success), executada em janela de manutenção pré-comunicada.
583
+
584
+ ### Anti-pattern 5: Cleanup imediato após move (sem 7d)
585
+
586
+ **Errado:**
587
+ ```sql
588
+ -- Logo após Supavisor reroute (REQ TENANT-05 passo 3)
589
+ drop schema tenant_<uuid> cascade;
590
+ ```
591
+
592
+ **Por quê:** se instância dedicada tiver bug não detectado (RLS quebrada, schema diverge, performance pior), você não consegue rollback. Customer fica fora do ar até nova restore from backup (RTO horas).
593
+
594
+ **Certo:** 7d de monitoring ativo (`pg_stat_user_tables.last_seq_scan`/`last_idx_scan` confirmados zero) antes do drop. Custo: 7d de storage duplicado (negligível vs custo de outage).
595
+
596
+ ## Ver também
597
+
598
+ - [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) — glossário compartilhado da Suíte DDIA Foundations v1.22 (define `hot spot`, `scatter-gather`, `consistent hashing`, `key range partitioning`, etc.)
599
+ - [`../multi-tenant-performance-scaling/SKILL.md`](../multi-tenant-performance-scaling/SKILL.md) — Supavisor pooling, partial indexes, helper functions STABLE (skill irmã v1.21 — base de scaling antes de mitigação de hot tenant)
600
+ - [`../supabase-postgres-style/SKILL.md`](../supabase-postgres-style/SKILL.md) — style guide SQL canônico (snake_case, schema-qualified, `private.*` para helpers)
601
+ - [`../multi-tenant-rls-hierarchy/SKILL.md`](../multi-tenant-rls-hierarchy/SKILL.md) — RLS hierarchical policies que coexistem com partições (RLS aplicada na tabela parent propaga para todas as partições)
602
+ - [`../super-admin-platform-pattern/SKILL.md`](../super-admin-platform-pattern/SKILL.md) — cross-tenant views super-admin (caso canônico para REQ TENANT-04 term-partitioned trade-off)
603
+ - DDIA Ch 6 (Designing Data-Intensive Applications, Martin Kleppmann) — Partitioning. Justin Bieber tenant: p.196 nota [13]. Hash vs range: p.194-196. Secondary indexes: p.197-200. Rebalancing: p.201-204.
604
+ - [Postgres Declarative Partitioning Docs](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE)
605
+ - [Supavisor Multi-Pool Docs](https://supabase.com/docs/guides/database/connecting-to-postgres#supavisor)