@luanpdd/kit-mcp 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/gates/agent-no-recursive-dispatch.md +82 -82
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/README.md +76 -76
  6. package/kit/agents/advisor-researcher.md +106 -106
  7. package/kit/agents/assumptions-analyzer.md +107 -107
  8. package/kit/agents/audit-log-implementer.md +313 -313
  9. package/kit/agents/auditor-consistencia-isolamento.md +413 -413
  10. package/kit/agents/b2b-saas-architect.md +156 -156
  11. package/kit/agents/cascading-failures-auditor.md +298 -298
  12. package/kit/agents/codebase-mapper.md +768 -768
  13. package/kit/agents/crm-pipeline-implementer.md +256 -256
  14. package/kit/agents/debugger.md +813 -813
  15. package/kit/agents/detector-tenant-quente.md +337 -337
  16. package/kit/agents/evolution-go-integrator.md +200 -200
  17. package/kit/agents/example-reviewer.md +21 -21
  18. package/kit/agents/executor.md +564 -564
  19. package/kit/agents/integration-checker.md +200 -200
  20. package/kit/agents/invite-flow-implementer.md +189 -189
  21. package/kit/agents/legacy-characterizer.md +368 -368
  22. package/kit/agents/lgpd-compliance-auditor.md +295 -295
  23. package/kit/agents/multi-tenant-isolation-auditor.md +253 -253
  24. package/kit/agents/multi-tenant-rls-writer.md +340 -340
  25. package/kit/agents/nyquist-auditor.md +178 -178
  26. package/kit/agents/observability-coverage-auditor.md +315 -315
  27. package/kit/agents/org-onboarding-implementer.md +223 -223
  28. package/kit/agents/payload-capture-instrumenter.md +273 -273
  29. package/kit/agents/phase-researcher.md +696 -696
  30. package/kit/agents/plan-checker.md +272 -272
  31. package/kit/agents/planner.md +922 -922
  32. package/kit/agents/project-researcher.md +652 -652
  33. package/kit/agents/refactor-safety-auditor.md +404 -404
  34. package/kit/agents/research-synthesizer.md +245 -245
  35. package/kit/agents/roadmapper.md +677 -677
  36. package/kit/agents/seam-finder.md +359 -359
  37. package/kit/agents/shotgun-surgery-detector.md +349 -349
  38. package/kit/agents/supabase-branching-architect.md +562 -562
  39. package/kit/agents/supabase-cicd-pipeline-implementer.md +777 -777
  40. package/kit/agents/supabase-column-privileges-writer.md +399 -399
  41. package/kit/agents/supabase-edge-fn-tester.md +287 -0
  42. package/kit/agents/supabase-edge-fn-writer.md +239 -210
  43. package/kit/agents/supabase-migration-writer.md +385 -385
  44. package/kit/agents/supabase-rbac-implementer.md +392 -392
  45. package/kit/agents/supabase-realtime-implementer.md +363 -267
  46. package/kit/agents/supabase-rls-hardener.md +521 -521
  47. package/kit/agents/supabase-rls-writer.md +323 -323
  48. package/kit/agents/supabase-roles-implementer.md +355 -355
  49. package/kit/agents/super-admin-implementer.md +281 -281
  50. package/kit/agents/ui-auditor.md +437 -437
  51. package/kit/agents/ui-checker.md +302 -302
  52. package/kit/agents/ui-researcher.md +355 -355
  53. package/kit/agents/user-profiler.md +175 -175
  54. package/kit/agents/validador-evolucao-schema.md +335 -335
  55. package/kit/agents/verifier.md +728 -728
  56. package/kit/commands/adicionar-backlog.md +75 -75
  57. package/kit/commands/adicionar-fase.md +42 -42
  58. package/kit/commands/adicionar-tarefa.md +45 -45
  59. package/kit/commands/adicionar-testes.md +41 -41
  60. package/kit/commands/ajuda.md +21 -21
  61. package/kit/commands/atualizar.md +37 -37
  62. package/kit/commands/auditar-cascading.md +111 -111
  63. package/kit/commands/auditar-marco.md +179 -179
  64. package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
  65. package/kit/commands/auditar-refactor.md +219 -219
  66. package/kit/commands/auditar-release.md +109 -109
  67. package/kit/commands/auditar-uat.md +23 -23
  68. package/kit/commands/autonomo.md +40 -40
  69. package/kit/commands/branch-pr.md +24 -24
  70. package/kit/commands/burn-rate-status.md +408 -408
  71. package/kit/commands/capturar-payloads.md +193 -193
  72. package/kit/commands/caracterizar.md +212 -212
  73. package/kit/commands/concluir-marco.md +247 -247
  74. package/kit/commands/configuracoes.md +36 -36
  75. package/kit/commands/dados-distribuidos.md +188 -188
  76. package/kit/commands/definir-perfil.md +10 -10
  77. package/kit/commands/depurar.md +190 -190
  78. package/kit/commands/detectar-duplicacao.md +197 -197
  79. package/kit/commands/discutir-fase.md +131 -131
  80. package/kit/commands/encontrar-seams.md +136 -136
  81. package/kit/commands/entrar-discord.md +17 -17
  82. package/kit/commands/estatisticas.md +18 -18
  83. package/kit/commands/example-greeting.md +33 -33
  84. package/kit/commands/executar-fase.md +58 -58
  85. package/kit/commands/expresso.md +56 -56
  86. package/kit/commands/fase-ui.md +34 -34
  87. package/kit/commands/fazer.md +57 -57
  88. package/kit/commands/fio.md +125 -125
  89. package/kit/commands/fluxos-trabalho.md +64 -64
  90. package/kit/commands/forense.md +176 -176
  91. package/kit/commands/gerenciador.md +38 -38
  92. package/kit/commands/inserir-fase.md +31 -31
  93. package/kit/commands/legacy.md +263 -263
  94. package/kit/commands/limpeza.md +17 -17
  95. package/kit/commands/listar-hipoteses-fase.md +45 -45
  96. package/kit/commands/listar-workspaces.md +18 -18
  97. package/kit/commands/load-shedding.md +117 -117
  98. package/kit/commands/mapear-codebase.md +70 -70
  99. package/kit/commands/multi-tenant.md +163 -163
  100. package/kit/commands/nota.md +33 -33
  101. package/kit/commands/novo-marco.md +43 -43
  102. package/kit/commands/novo-projeto.md +41 -41
  103. package/kit/commands/novo-workspace.md +43 -43
  104. package/kit/commands/pausar-trabalho.md +37 -37
  105. package/kit/commands/perfil-usuario.md +45 -45
  106. package/kit/commands/pesquisar-fase.md +195 -195
  107. package/kit/commands/planejar-fase.md +67 -67
  108. package/kit/commands/planejar-lacunas.md +33 -33
  109. package/kit/commands/plantar-ideia.md +25 -25
  110. package/kit/commands/progresso.md +24 -24
  111. package/kit/commands/proximo.md +30 -30
  112. package/kit/commands/publicar.md +490 -490
  113. package/kit/commands/rapido.md +35 -35
  114. package/kit/commands/reaplicar-patches.md +124 -124
  115. package/kit/commands/refactor-seguro.md +321 -321
  116. package/kit/commands/relatorio-sessao.md +19 -19
  117. package/kit/commands/remover-fase.md +31 -31
  118. package/kit/commands/remover-workspace.md +26 -26
  119. package/kit/commands/resumo-marco.md +50 -50
  120. package/kit/commands/retomar-trabalho.md +40 -40
  121. package/kit/commands/revisar-backlog.md +60 -60
  122. package/kit/commands/revisar-ui.md +32 -32
  123. package/kit/commands/revisar.md +37 -37
  124. package/kit/commands/saude.md +21 -21
  125. package/kit/commands/setup-notion.md +93 -93
  126. package/kit/commands/storytelling.md +179 -179
  127. package/kit/commands/supabase.md +30 -7
  128. package/kit/commands/sync-main.md +68 -68
  129. package/kit/commands/validar-fase.md +35 -35
  130. package/kit/commands/verificar-tarefas.md +44 -44
  131. package/kit/commands/verificar-trabalho.md +64 -64
  132. package/kit/file-manifest.json +14 -8
  133. package/kit/framework/bin/lib/commands.cjs +959 -959
  134. package/kit/framework/bin/lib/config.cjs +442 -442
  135. package/kit/framework/bin/lib/core.cjs +1230 -1230
  136. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  137. package/kit/framework/bin/lib/init.cjs +1442 -1442
  138. package/kit/framework/bin/lib/milestone.cjs +252 -252
  139. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  140. package/kit/framework/bin/lib/phase.cjs +888 -888
  141. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  142. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  143. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  144. package/kit/framework/bin/lib/security.cjs +382 -382
  145. package/kit/framework/bin/lib/state.cjs +1031 -1031
  146. package/kit/framework/bin/lib/template.cjs +222 -222
  147. package/kit/framework/bin/lib/uat.cjs +282 -282
  148. package/kit/framework/bin/lib/verify.cjs +888 -888
  149. package/kit/framework/bin/lib/workstream.cjs +491 -491
  150. package/kit/framework/bin/tools.cjs +918 -918
  151. package/kit/framework/commands/workstreams.md +63 -63
  152. package/kit/framework/references/checkpoints.md +778 -778
  153. package/kit/framework/references/continuation-format.md +249 -249
  154. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  155. package/kit/framework/references/git-integration.md +295 -295
  156. package/kit/framework/references/git-planning-commit.md +38 -38
  157. package/kit/framework/references/model-profile-resolution.md +36 -36
  158. package/kit/framework/references/model-profiles.md +139 -139
  159. package/kit/framework/references/phase-argument-parsing.md +61 -61
  160. package/kit/framework/references/planning-config.md +202 -202
  161. package/kit/framework/references/questioning.md +162 -162
  162. package/kit/framework/references/tdd.md +263 -263
  163. package/kit/framework/references/ui-brand.md +160 -160
  164. package/kit/framework/references/user-profiling.md +657 -657
  165. package/kit/framework/references/verification-patterns.md +612 -612
  166. package/kit/framework/references/workstream-flag.md +58 -58
  167. package/kit/framework/templates/DEBUG.md +164 -164
  168. package/kit/framework/templates/UAT.md +265 -265
  169. package/kit/framework/templates/UI-SPEC.md +100 -100
  170. package/kit/framework/templates/VALIDATION.md +76 -76
  171. package/kit/framework/templates/claude-md.md +122 -122
  172. package/kit/framework/templates/codebase/architecture.md +185 -185
  173. package/kit/framework/templates/codebase/concerns.md +205 -205
  174. package/kit/framework/templates/codebase/conventions.md +204 -204
  175. package/kit/framework/templates/codebase/integrations.md +192 -192
  176. package/kit/framework/templates/codebase/stack.md +158 -158
  177. package/kit/framework/templates/codebase/structure.md +199 -199
  178. package/kit/framework/templates/codebase/testing.md +301 -301
  179. package/kit/framework/templates/config.json +44 -44
  180. package/kit/framework/templates/context.md +352 -352
  181. package/kit/framework/templates/continue-here.md +78 -78
  182. package/kit/framework/templates/copilot-instructions.md +7 -7
  183. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  184. package/kit/framework/templates/dev-preferences.md +20 -20
  185. package/kit/framework/templates/discovery.md +146 -146
  186. package/kit/framework/templates/discussion-log.md +63 -63
  187. package/kit/framework/templates/milestone-archive.md +123 -123
  188. package/kit/framework/templates/milestone.md +115 -115
  189. package/kit/framework/templates/phase-prompt.md +610 -610
  190. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  191. package/kit/framework/templates/project.md +186 -186
  192. package/kit/framework/templates/requirements.md +231 -231
  193. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  194. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  195. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  196. package/kit/framework/templates/research-project/STACK.md +120 -120
  197. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  198. package/kit/framework/templates/research.md +419 -419
  199. package/kit/framework/templates/retrospective.md +54 -54
  200. package/kit/framework/templates/roadmap.md +202 -202
  201. package/kit/framework/templates/state.md +176 -176
  202. package/kit/framework/templates/summary-complex.md +59 -59
  203. package/kit/framework/templates/summary-minimal.md +41 -41
  204. package/kit/framework/templates/summary-standard.md +48 -48
  205. package/kit/framework/templates/summary.md +209 -209
  206. package/kit/framework/templates/user-profile.md +146 -146
  207. package/kit/framework/templates/user-setup.md +256 -256
  208. package/kit/framework/templates/verification-report.md +258 -258
  209. package/kit/framework/workflows/add-phase.md +112 -112
  210. package/kit/framework/workflows/add-tests.md +351 -351
  211. package/kit/framework/workflows/add-todo.md +158 -158
  212. package/kit/framework/workflows/audit-milestone.md +340 -340
  213. package/kit/framework/workflows/audit-uat.md +109 -109
  214. package/kit/framework/workflows/autonomous.md +891 -891
  215. package/kit/framework/workflows/check-todos.md +177 -177
  216. package/kit/framework/workflows/cleanup.md +152 -152
  217. package/kit/framework/workflows/complete-milestone.md +696 -696
  218. package/kit/framework/workflows/diagnose-issues.md +231 -231
  219. package/kit/framework/workflows/discovery-phase.md +289 -289
  220. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  221. package/kit/framework/workflows/discuss-phase.md +784 -784
  222. package/kit/framework/workflows/do.md +104 -104
  223. package/kit/framework/workflows/execute-phase.md +838 -838
  224. package/kit/framework/workflows/execute-plan.md +510 -510
  225. package/kit/framework/workflows/fast.md +102 -102
  226. package/kit/framework/workflows/forensics.md +265 -265
  227. package/kit/framework/workflows/health.md +181 -181
  228. package/kit/framework/workflows/help.md +619 -619
  229. package/kit/framework/workflows/insert-phase.md +130 -130
  230. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  231. package/kit/framework/workflows/list-workspaces.md +56 -56
  232. package/kit/framework/workflows/manager.md +362 -362
  233. package/kit/framework/workflows/map-codebase.md +377 -377
  234. package/kit/framework/workflows/milestone-summary.md +223 -223
  235. package/kit/framework/workflows/new-milestone.md +486 -486
  236. package/kit/framework/workflows/new-project.md +1159 -1159
  237. package/kit/framework/workflows/new-workspace.md +237 -237
  238. package/kit/framework/workflows/next.md +97 -97
  239. package/kit/framework/workflows/node-repair.md +92 -92
  240. package/kit/framework/workflows/note.md +156 -156
  241. package/kit/framework/workflows/pause-work.md +176 -176
  242. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  243. package/kit/framework/workflows/plan-phase.md +765 -765
  244. package/kit/framework/workflows/plant-seed.md +169 -169
  245. package/kit/framework/workflows/pr-branch.md +129 -129
  246. package/kit/framework/workflows/profile-user.md +450 -450
  247. package/kit/framework/workflows/progress.md +507 -507
  248. package/kit/framework/workflows/quick.md +757 -757
  249. package/kit/framework/workflows/remove-phase.md +155 -155
  250. package/kit/framework/workflows/remove-workspace.md +90 -90
  251. package/kit/framework/workflows/research-phase.md +82 -82
  252. package/kit/framework/workflows/resume-project.md +326 -326
  253. package/kit/framework/workflows/review.md +228 -228
  254. package/kit/framework/workflows/session-report.md +146 -146
  255. package/kit/framework/workflows/settings.md +283 -283
  256. package/kit/framework/workflows/ship.md +228 -228
  257. package/kit/framework/workflows/stats.md +60 -60
  258. package/kit/framework/workflows/transition.md +671 -671
  259. package/kit/framework/workflows/ui-phase.md +302 -302
  260. package/kit/framework/workflows/ui-review.md +165 -165
  261. package/kit/framework/workflows/update.md +323 -323
  262. package/kit/framework/workflows/validate-phase.md +174 -174
  263. package/kit/framework/workflows/verify-phase.md +252 -252
  264. package/kit/framework/workflows/verify-work.md +637 -637
  265. package/kit/hooks/check-update.js +118 -118
  266. package/kit/hooks/context-monitor.js +163 -163
  267. package/kit/hooks/prompt-guard.js +103 -103
  268. package/kit/hooks/statusline.js +125 -125
  269. package/kit/hooks/workflow-guard.js +101 -101
  270. package/kit/settings.json +45 -45
  271. package/kit/skills/_shared-supabase/glossary.md +17 -0
  272. package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
  273. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
  274. package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
  275. package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
  276. package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
  277. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
  278. package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
  279. package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
  280. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
  281. package/kit/skills/example-skill/SKILL.md +42 -42
  282. package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
  283. package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
  284. package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
  285. package/kit/skills/legacy-extract-class/SKILL.md +203 -203
  286. package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
  287. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
  288. package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
  289. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
  290. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
  291. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
  292. package/kit/skills/member-invite-flow/SKILL.md +305 -305
  293. package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
  294. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
  295. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
  296. package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
  297. package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
  298. package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
  299. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
  300. package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
  301. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
  302. package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
  303. package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
  304. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
  305. package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
  306. package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
  307. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
  308. package/kit/skills/supabase-edge-functions/SKILL.md +229 -141
  309. package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -0
  310. package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -0
  311. package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -0
  312. package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -0
  313. package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -0
  314. package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
  315. package/kit/skills/supabase-migrations/SKILL.md +297 -297
  316. package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
  317. package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
  318. package/kit/skills/supabase-realtime/SKILL.md +460 -236
  319. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
  320. package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
  321. package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
  322. package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
  323. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
  324. package/package.json +1 -1
  325. package/src/core/kit.js +216 -216
  326. package/src/core/reflect.js +247 -247
  327. package/src/core/reverse-sync.js +372 -372
  328. package/src/core/sync.js +418 -418
  329. package/src/core/watch.js +121 -121
  330. package/src/mcp-server/index.js +693 -693
@@ -1,472 +1,472 @@
1
- ---
2
- name: supabase-custom-claims-rbac
3
- description: Use ao implementar Custom Claims via Custom Access Token Auth Hook para RBAC em Supabase…
4
- ---
5
-
6
- # Supabase — Custom Claims & RBAC via Auth Hooks
7
-
8
- ## Quando usar
9
-
10
- LLM carrega esta skill quando implementar **Role-Based Access Control (RBAC)** via Custom Claims no JWT. Pattern canônico Supabase v1.25.
11
-
12
- Trigger phrases:
13
-
14
- - "custom claims Supabase", "RBAC Supabase"
15
- - "Custom Access Token Auth Hook", "auth hook RBAC"
16
- - "user_role no JWT", "authorize() function"
17
- - "role-based access control com auth hook"
18
- - "como evitar JOIN em RLS policy" (custom claims é a alternativa moderna)
19
-
20
- ## Princípio canônico
21
-
22
- RBAC em Supabase tem **3 mecanismos de delivery** dos claims:
23
-
24
- 1. **`app_metadata`** (skill `supabase-rls-policies` v1.23) — JSONB no JWT setado via service_role admin API. Simples mas limitado (não suporta enum types, sem normalização).
25
- 2. **Dedicated role table com helper function STABLE** (skill `rbac-permissions-matrix-supabase` v1.21) — `user_roles` + `private.has_role(role)` consultada em policies. Dinâmico mas faz JOIN custoso por query.
26
- 3. **Custom Claims via Custom Access Token Auth Hook** (v1.25 — **RECOMENDADO**) — `user_role` injetado no JWT durante geração do token via auth hook. Evita JOIN em policies (claim lido direto via `auth.jwt() ->> 'user_role'`). Eventually consistent (refresh TTL).
27
-
28
- **Quando usar custom claims (v1.25):**
29
-
30
- - ✅ RBAC com 2-10 roles fixos por user (admin, moderator, user, etc.)
31
- - ✅ Permission matrix relativamente estática (mudanças em horas/dias, não segundos)
32
- - ✅ Policies que precisam ser rápidas (sem JOIN custoso)
33
- - ✅ Cliente front-end precisa saber o role (UI conditional rendering)
34
-
35
- **Quando NÃO usar custom claims:**
36
-
37
- - ❌ Permission mudada em real-time (segundos) — JWT freshness não cobre
38
- - ❌ Permission depende de row context (ex: "can edit if owner of this row") — use RLS row-level com `auth.uid()`
39
- - ❌ Multi-tenant com user em N orgs com role diferente em cada — custom claim é per-user, não per-org-context
40
-
41
- Para multi-tenant complexo, combine: custom claim para role global (super_admin) + RLS hierárquica com `private.has_role(role, org_id)` para context-aware (skill `multi-tenant-rls-hierarchy`).
42
-
43
- ## Pattern canônico — 7 passos
44
-
45
- ### Passo 1: Enum types
46
-
47
- Defina enum types Postgres para roles e permissions — tipo-seguro, refactorable, documentado.
48
-
49
- ```sql
50
- -- enum de roles canônicos
51
- create type public.app_role as enum ('admin', 'moderator', 'user');
52
-
53
- -- enum de permissions canônicos (resource.action)
54
- create type public.app_permission as enum (
55
- 'channels.delete',
56
- 'channels.create',
57
- 'messages.delete',
58
- 'messages.update',
59
- 'users.ban'
60
- );
61
- ```
62
-
63
- **Caveat:** adicionar novo enum value exige migration `alter type public.app_permission add value 'novo.permission'` — não pode ser feito dentro de transação (Postgres limitation).
64
-
65
- ### Passo 2: Tabelas user_roles + role_permissions
66
-
67
- ```sql
68
- -- USER → ROLE mapping (N:1 user pode ter múltiplos roles)
69
- create table public.user_roles (
70
- id bigint generated by default as identity primary key,
71
- user_id uuid references auth.users on delete cascade not null,
72
- role app_role not null,
73
- unique (user_id, role)
74
- );
75
- comment on table public.user_roles is 'Application roles for each user.';
76
-
77
- -- ROLE → PERMISSION mapping (N:N role tem múltiplas permissions)
78
- create table public.role_permissions (
79
- id bigint generated by default as identity primary key,
80
- role app_role not null,
81
- permission app_permission not null,
82
- unique (role, permission)
83
- );
84
- comment on table public.role_permissions is 'Application permissions for each role.';
85
- ```
86
-
87
- Seed inicial:
88
-
89
- ```sql
90
- insert into public.role_permissions (role, permission)
91
- values
92
- ('admin', 'channels.delete'),
93
- ('admin', 'channels.create'),
94
- ('admin', 'messages.delete'),
95
- ('admin', 'messages.update'),
96
- ('admin', 'users.ban'),
97
- ('moderator', 'messages.delete'),
98
- ('moderator', 'messages.update');
99
- ```
100
-
101
- ### Passo 3: Custom Access Token Auth Hook function
102
-
103
- A função `custom_access_token_hook(event jsonb) returns jsonb` roda **antes** do JWT ser issued — recebe o event (com `user_id` + claims atuais) e devolve event modificado.
104
-
105
- ```sql
106
- create or replace function public.custom_access_token_hook(event jsonb)
107
- returns jsonb
108
- language plpgsql
109
- stable
110
- as $$
111
- declare
112
- claims jsonb;
113
- user_role public.app_role;
114
- begin
115
- -- buscar role do user em user_roles
116
- select role into user_role
117
- from public.user_roles
118
- where user_id = (event->>'user_id')::uuid;
119
-
120
- claims := event->'claims';
121
-
122
- if user_role is not null then
123
- claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
124
- else
125
- claims := jsonb_set(claims, '{user_role}', 'null');
126
- end if;
127
-
128
- -- atualiza objeto claims no event original
129
- event := jsonb_set(event, '{claims}', claims);
130
-
131
- return event;
132
- end;
133
- $$;
134
- ```
135
-
136
- **Caveat para multi-role:** se user tem múltiplos roles em `user_roles`, esta versão pega o primeiro (`select role into user_role`). Para múltiplos roles no JWT, use:
137
-
138
- ```sql
139
- -- variante multi-role: claim user_roles como array
140
- select array_agg(role) into user_roles_array
141
- from public.user_roles
142
- where user_id = (event->>'user_id')::uuid;
143
- -- ...
144
- claims := jsonb_set(claims, '{user_roles}', to_jsonb(user_roles_array));
145
- ```
146
-
147
- ### Passo 4: Permissions canônicos para `supabase_auth_admin`
148
-
149
- `supabase_auth_admin` é o Postgres role usado pelo Supabase Auth service ao invocar o hook. Precisa de permissions específicos:
150
-
151
- ```sql
152
- -- 1. acesso ao schema public
153
- grant usage on schema public to supabase_auth_admin;
154
-
155
- -- 2. permissão de EXECUTE no hook function (apenas auth_admin pode invocar)
156
- grant execute
157
- on function public.custom_access_token_hook
158
- to supabase_auth_admin;
159
-
160
- -- 3. REVOKE EXECUTE de roles públicos (cliente NÃO pode chamar diretamente)
161
- revoke execute
162
- on function public.custom_access_token_hook
163
- from authenticated, anon, public;
164
-
165
- -- 4. acesso à tabela user_roles
166
- grant all
167
- on table public.user_roles
168
- to supabase_auth_admin;
169
-
170
- -- 5. REVOKE acesso público à user_roles (cliente NÃO pode mutar roles)
171
- revoke all
172
- on table public.user_roles
173
- from authenticated, anon, public;
174
-
175
- -- 6. RLS policy permitindo supabase_auth_admin ler user_roles
176
- create policy "Allow auth admin to read user roles" on public.user_roles
177
- as permissive for select
178
- to supabase_auth_admin
179
- using (true);
180
- ```
181
-
182
- **Por quê REVOKE EXECUTE do hook:** sem isso, qualquer cliente autenticado poderia chamar `custom_access_token_hook(...)` e potencialmente abusar.
183
-
184
- **Por quê REVOKE ALL FROM authenticated em user_roles:** roles são metadado privilegiado — apenas admins (service_role) podem inserir/atualizar. Cliente não deve ter SELECT direto na tabela (consultar via claim do JWT).
185
-
186
- ### Passo 5: Habilitar o hook
187
-
188
- **Dashboard (production):**
189
-
190
- 1. Acesse `Authentication > Hooks (Beta)` no Dashboard Supabase
191
- 2. Selecione "Custom Access Token" hook type
192
- 3. Dropdown: selecione `public.custom_access_token_hook` (função criada no Passo 3)
193
- 4. Save
194
-
195
- **Local development:**
196
-
197
- Em `supabase/config.toml`:
198
-
199
- ```toml
200
- [auth.hook.custom_access_token]
201
- enabled = true
202
- uri = "pg-functions://postgres/public/custom_access_token_hook"
203
- ```
204
-
205
- Reinicie o Supabase local (`supabase stop && supabase start`).
206
-
207
- ### Passo 6: `authorize()` function
208
-
209
- A função `authorize(permission app_permission) returns boolean` é o coração do RBAC — lê `user_role` do JWT e checa se o role tem a permission.
210
-
211
- ```sql
212
- create or replace function public.authorize(
213
- requested_permission app_permission
214
- )
215
- returns boolean
216
- language plpgsql
217
- stable
218
- security definer
219
- set search_path = ''
220
- as $$
221
- declare
222
- bind_permissions int;
223
- user_role public.app_role;
224
- begin
225
- -- ler user_role do JWT (delivered via auth hook do Passo 3)
226
- select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
227
-
228
- -- contar permissions matching
229
- select count(*)
230
- into bind_permissions
231
- from public.role_permissions
232
- where role_permissions.permission = requested_permission
233
- and role_permissions.role = user_role;
234
-
235
- return bind_permissions > 0;
236
- end;
237
- $$;
238
- ```
239
-
240
- **Decisões canônicas:**
241
-
242
- - `stable` — função consulta DB mas não muta; resultado é estável dentro de uma transação (Postgres pode cachear)
243
- - `security definer` — roda com privilégios do owner (geralmente `postgres`) — necessário para acessar role_permissions sem RLS recursivo
244
- - `set search_path = ''` — anti schema injection (cross-ref skill `supabase-database-functions`)
245
-
246
- ### Passo 7: RLS policies usando authorize()
247
-
248
- Em vez de hard-code role em policy, use `authorize(permission)`:
249
-
250
- ```sql
251
- -- ANTES (anti-pattern): hard-code role direto na policy
252
- create policy "Allow admin delete channels" on public.channels for delete
253
- to authenticated
254
- using ((auth.jwt() ->> 'user_role') = 'admin');
255
-
256
- -- DEPOIS (canônico v1.25): authorize() abstrai role → permission
257
- create policy "Allow authorized delete access" on public.channels for delete
258
- to authenticated
259
- using ((SELECT authorize('channels.delete')));
260
-
261
- create policy "Allow authorized delete access" on public.messages for delete
262
- to authenticated
263
- using ((SELECT authorize('messages.delete')));
264
- ```
265
-
266
- **Vantagens canônicas:**
267
-
268
- - Adicionar nova permission = INSERT em role_permissions (sem alterar policy)
269
- - Mudar quem tem permission = UPDATE em role_permissions
270
- - Policies ficam estáveis; matriz permissions evolui
271
- - Reutilizável em N policies
272
-
273
- **Por que `(SELECT authorize(...))` com wrapper:** caching (cross-ref REGRA #2 da skill `supabase-rls-policies` v1.23) — função roda 1 vez por query, não 1 vez por linha.
274
-
275
- ## Client-side — acessar custom claims
276
-
277
- Auth hook só modifica o **access_token JWT**, não a auth response. Para acessar custom claims no cliente, decode o JWT:
278
-
279
- ```bash
280
- npm install jwt-decode
281
- ```
282
-
283
- ```js
284
- import { jwtDecode } from 'jwt-decode'
285
- import { createClient } from '@supabase/supabase-js'
286
-
287
- const supabase = createClient(URL, ANON_KEY)
288
-
289
- // listener para mudanças de auth state (login, logout, refresh)
290
- supabase.auth.onAuthStateChange(async (event, session) => {
291
- if (session) {
292
- const jwt = jwtDecode(session.access_token)
293
- const userRole = jwt.user_role // 'admin' | 'moderator' | 'user' | null
294
-
295
- // usar userRole para UI conditional rendering
296
- if (userRole === 'admin') {
297
- // show admin panel
298
- }
299
- }
300
- })
301
- ```
302
-
303
- **Para Next.js v16 + SSR:** decode o JWT no server-side via middleware ou Server Component:
304
-
305
- ```ts
306
- // app/middleware.ts ou Server Component
307
- import { jwtDecode } from 'jwt-decode'
308
-
309
- const { data: { session } } = await supabase.auth.getSession()
310
- if (session) {
311
- const jwt = jwtDecode(session.access_token)
312
- const userRole = jwt.user_role
313
- // ...
314
- }
315
- ```
316
-
317
- **Para backend (Node.js/Python/etc):** use bibliotecas equivalentes:
318
-
319
- - Node.js: `jsonwebtoken`, `express-jwt`, `koa-jwt`
320
- - Python: `PyJWT`
321
- - Dart: `dart_jsonwebtoken`
322
- - .NET: `Microsoft.AspNetCore.Authentication.JwtBearer`
323
-
324
- ## ⚠ Caveat — JWT Freshness
325
-
326
- Mudanças em `user_roles` **NÃO** refletem imediatamente no JWT do user — apenas após **token refresh** (default TTL 1h em Supabase).
327
-
328
- **Cenários:**
329
-
330
- | Situação | Comportamento |
331
- |----------|---------------|
332
- | Admin adiciona role moderator ao user X | User X continua com `user_role: null` até próximo refresh (até 1h) |
333
- | Admin remove role admin do user Y | User Y continua com `user_role: admin` no JWT atual; após refresh, policies bloqueiam |
334
- | Permission adicionada em role_permissions (admin → users.ban) | Reflete IMEDIATAMENTE — authorize() consulta DB cada query (stable mas re-executa em query nova) |
335
-
336
- **Implicações:**
337
-
338
- - Adicionar role: aceitável demora (user pode logout/login para forçar refresh)
339
- - **Remover role: PROBLEMA** — user comprometido pode usar JWT antigo até expirar. Para invalidação imediata:
340
-
341
- ```ts
342
- // force logout via admin API (server-side com service_role)
343
- await supabase.auth.admin.signOut(userId)
344
- // próximo request do user vai falhar; user precisa login novamente
345
- ```
346
-
347
- - Mudança em permissions matrix (role_permissions): IMEDIATA — não precisa refresh
348
-
349
- ## Anti-patterns
350
-
351
- ### Anti-pattern 1: Esquecer GRANT EXECUTE ao supabase_auth_admin
352
-
353
- **Errado:**
354
- ```sql
355
- create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ...;
356
- -- esqueceu o GRANT EXECUTE TO supabase_auth_admin
357
- ```
358
-
359
- **Por quê:** auth hook falha silenciosamente — JWT é issued mas SEM o claim `user_role`. Difícil de debug.
360
-
361
- **Certo:** sempre incluir os 6 GRANTs/REVOKEs do Passo 4.
362
-
363
- ### Anti-pattern 2: Hardcode role em policy ao invés de authorize()
364
-
365
- **Errado:**
366
- ```sql
367
- create policy "admin_delete" on public.channels for delete to authenticated
368
- using ((auth.jwt() ->> 'user_role') = 'admin');
369
- ```
370
-
371
- **Por quê:** acoplamento policy ↔ role. Mudar quem pode deletar requer ALTER POLICY (DDL); com authorize(), basta UPDATE em role_permissions.
372
-
373
- **Certo:** `using ((SELECT authorize('channels.delete')))` — policy estável, matriz evolui.
374
-
375
- ### Anti-pattern 3: Assumir JWT fresh sem invalidação
376
-
377
- **Errado:**
378
- ```ts
379
- // admin remove role admin do user X
380
- await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
381
- // CRÍTICO: user X ainda tem JWT antigo válido por até 1h
382
- ```
383
-
384
- **Por quê:** revogação de role não é imediata. Em casos de comprometimento, atacante usa o JWT antigo para abusar de privilégios admin.
385
-
386
- **Certo:** sempre force logout após mudança crítica de role:
387
- ```ts
388
- await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
389
- await supabase.auth.admin.signOut(xId) // força refresh imediato
390
- ```
391
-
392
- ### Anti-pattern 4: Mutar app_metadata do cliente para mudar role
393
-
394
- **Errado:**
395
- ```ts
396
- // no cliente
397
- await supabase.auth.updateUser({ data: { role: 'admin' } })
398
- // user_metadata é editável pelo cliente — privilege escalation
399
- ```
400
-
401
- **Por quê:** mesmo anti-pattern de `user_metadata` da skill `supabase-rls-policies` (REGRA #1). Pior — confunde "role via custom claim" com "role via user_metadata".
402
-
403
- **Certo:** roles são mutados APENAS via INSERT em `public.user_roles` por backend (service_role) ou admin API. Cliente nunca toca em metadata de role.
404
-
405
- ### Anti-pattern 5: Auth hook que faz query custosa
406
-
407
- **Errado:**
408
- ```sql
409
- create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ... as $$
410
- begin
411
- -- query custosa em cada token issuance
412
- select * into user_data from public.users
413
- join public.organizations o on u.org_id = o.id
414
- join public.subscriptions s on o.id = s.org_id
415
- where u.id = (event->>'user_id')::uuid;
416
- -- ...
417
- end;
418
- $$;
419
- ```
420
-
421
- **Por quê:** hook roda em **cada login + cada refresh**. Query lenta degrada latência de auth. Bom hook é < 10ms.
422
-
423
- **Certo:** mantenha hook simples — query single table (user_roles), aggregate se necessário, sem JOINs. Se precisa de info complexa, materialize em coluna de user_roles (denormalize).
424
-
425
- ## Postgres Roles vs Custom Claims — distinção canônica (v1.26)
426
-
427
- **Custom Claims** (v1.25) e **Postgres Roles** (v1.26) são conceitos **complementares**, não substitutos:
428
-
429
- | | Custom Claims (v1.25) | Postgres Roles (v1.26) |
430
- |---|---|---|
431
- | Tipo de access | Application access (end-users) | System access (service accounts) |
432
- | Identidade | JWT claim `user_role` | Postgres login |
433
- | Quem usa | End-users via PostgREST | Cron jobs, BI tools, ETL, admin scripts |
434
- | Granularidade | Per-permission via `authorize()` | Per schema/table/function/column |
435
- | Dinamicidade | Eventually consistent (TTL refresh) | Estática (alterações via SQL DDL) |
436
- | Exemplo | "User admin pode deletar messages" | "Cron job pode SELECT em todas tabelas para backup" |
437
-
438
- **Combine quando ambos aplicam:**
439
-
440
- - End-user com role `admin` (via custom claim) acessa endpoint que invoca função `private.org_analytics()` com SECURITY DEFINER (que tem GRANT EXECUTE apenas para Postgres role `analytics_reader`)
441
- - Service account `metabase_reader` (Postgres role) acessa view `public.user_active_tasks` com `security_invoker=true` (que respeita RLS via auth.jwt() — mas auth.jwt() é null para Postgres role login, então policies precisam considerar service_role bypass)
442
-
443
- **NÃO use Postgres roles para application access:**
444
-
445
- - ❌ Criar role Postgres `app_admin` para gerenciar quem é admin no app
446
- - ❌ Criar role Postgres por end-user
447
- - ✅ Use RLS + Custom Claims via auth hook (pattern v1.25)
448
-
449
- **NÃO use Custom Claims para system access:**
450
-
451
- - ❌ Service account cron job autenticando via JWT customizado
452
- - ❌ BI tool authenticando via auth hook
453
- - ✅ Use Postgres role dedicado (pattern v1.26)
454
-
455
- Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
456
-
457
- ## Cross-suite integration (v1.25)
458
-
459
- Esta skill é base para o agent novo `supabase-rbac-implementer` (Phase 139) — recebe spec (roles + permissions matrix) via `Task()` e materializa setup completo. Pattern de handoff cooperativo herdado de v1.23/v1.24.
460
-
461
- Para multi-tenant com role por org, **combine** custom claim (role global) + helper function PG (`private.has_role(role, org_id)` em RLS hierárquica — skill `multi-tenant-rls-hierarchy` v1.21). Custom claim sozinho não cobre context-aware multi-tenant.
462
-
463
- ## Ver também
464
-
465
- - [supabase-rbac-implementer](../../agents/supabase-rbac-implementer.md) (v1.25) — canonical materializer
466
- - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) (v1.23) — section "RBAC via Custom Claims + authorize() function (v1.25)"
467
- - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) (v1.24) — Camada 9 (Auth Hooks - Custom Claims) v1.25
468
- - [supabase-database-functions](../supabase-database-functions/SKILL.md) — pattern SECURITY DEFINER + supabase_auth_admin grants
469
- - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — Next.js v16 + onAuthStateChange + jwt-decode integration
470
- - [supabase-rls-hardener](../../agents/supabase-rls-hardener.md) (v1.23) — Detector 9 valida auth hook instalado (v1.25)
471
- - [rbac-permissions-matrix-supabase](../rbac-permissions-matrix-supabase/SKILL.md) (v1.21) — alternativa via helper function STABLE (vs custom claim v1.25)
472
- - [glossário compartilhado](../_shared-supabase/glossary.md) — termos custom claims, Custom Access Token Auth Hook, JWT user_role claim, authorize() function, supabase_auth_admin role, app_role enum, app_permission enum, jwt-decode client pattern
1
+ ---
2
+ name: supabase-custom-claims-rbac
3
+ description: Use ao implementar Custom Claims via Custom Access Token Auth Hook para RBAC em Supabase…
4
+ ---
5
+
6
+ # Supabase — Custom Claims & RBAC via Auth Hooks
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando implementar **Role-Based Access Control (RBAC)** via Custom Claims no JWT. Pattern canônico Supabase v1.25.
11
+
12
+ Trigger phrases:
13
+
14
+ - "custom claims Supabase", "RBAC Supabase"
15
+ - "Custom Access Token Auth Hook", "auth hook RBAC"
16
+ - "user_role no JWT", "authorize() function"
17
+ - "role-based access control com auth hook"
18
+ - "como evitar JOIN em RLS policy" (custom claims é a alternativa moderna)
19
+
20
+ ## Princípio canônico
21
+
22
+ RBAC em Supabase tem **3 mecanismos de delivery** dos claims:
23
+
24
+ 1. **`app_metadata`** (skill `supabase-rls-policies` v1.23) — JSONB no JWT setado via service_role admin API. Simples mas limitado (não suporta enum types, sem normalização).
25
+ 2. **Dedicated role table com helper function STABLE** (skill `rbac-permissions-matrix-supabase` v1.21) — `user_roles` + `private.has_role(role)` consultada em policies. Dinâmico mas faz JOIN custoso por query.
26
+ 3. **Custom Claims via Custom Access Token Auth Hook** (v1.25 — **RECOMENDADO**) — `user_role` injetado no JWT durante geração do token via auth hook. Evita JOIN em policies (claim lido direto via `auth.jwt() ->> 'user_role'`). Eventually consistent (refresh TTL).
27
+
28
+ **Quando usar custom claims (v1.25):**
29
+
30
+ - ✅ RBAC com 2-10 roles fixos por user (admin, moderator, user, etc.)
31
+ - ✅ Permission matrix relativamente estática (mudanças em horas/dias, não segundos)
32
+ - ✅ Policies que precisam ser rápidas (sem JOIN custoso)
33
+ - ✅ Cliente front-end precisa saber o role (UI conditional rendering)
34
+
35
+ **Quando NÃO usar custom claims:**
36
+
37
+ - ❌ Permission mudada em real-time (segundos) — JWT freshness não cobre
38
+ - ❌ Permission depende de row context (ex: "can edit if owner of this row") — use RLS row-level com `auth.uid()`
39
+ - ❌ Multi-tenant com user em N orgs com role diferente em cada — custom claim é per-user, não per-org-context
40
+
41
+ Para multi-tenant complexo, combine: custom claim para role global (super_admin) + RLS hierárquica com `private.has_role(role, org_id)` para context-aware (skill `multi-tenant-rls-hierarchy`).
42
+
43
+ ## Pattern canônico — 7 passos
44
+
45
+ ### Passo 1: Enum types
46
+
47
+ Defina enum types Postgres para roles e permissions — tipo-seguro, refactorable, documentado.
48
+
49
+ ```sql
50
+ -- enum de roles canônicos
51
+ create type public.app_role as enum ('admin', 'moderator', 'user');
52
+
53
+ -- enum de permissions canônicos (resource.action)
54
+ create type public.app_permission as enum (
55
+ 'channels.delete',
56
+ 'channels.create',
57
+ 'messages.delete',
58
+ 'messages.update',
59
+ 'users.ban'
60
+ );
61
+ ```
62
+
63
+ **Caveat:** adicionar novo enum value exige migration `alter type public.app_permission add value 'novo.permission'` — não pode ser feito dentro de transação (Postgres limitation).
64
+
65
+ ### Passo 2: Tabelas user_roles + role_permissions
66
+
67
+ ```sql
68
+ -- USER → ROLE mapping (N:1 user pode ter múltiplos roles)
69
+ create table public.user_roles (
70
+ id bigint generated by default as identity primary key,
71
+ user_id uuid references auth.users on delete cascade not null,
72
+ role app_role not null,
73
+ unique (user_id, role)
74
+ );
75
+ comment on table public.user_roles is 'Application roles for each user.';
76
+
77
+ -- ROLE → PERMISSION mapping (N:N role tem múltiplas permissions)
78
+ create table public.role_permissions (
79
+ id bigint generated by default as identity primary key,
80
+ role app_role not null,
81
+ permission app_permission not null,
82
+ unique (role, permission)
83
+ );
84
+ comment on table public.role_permissions is 'Application permissions for each role.';
85
+ ```
86
+
87
+ Seed inicial:
88
+
89
+ ```sql
90
+ insert into public.role_permissions (role, permission)
91
+ values
92
+ ('admin', 'channels.delete'),
93
+ ('admin', 'channels.create'),
94
+ ('admin', 'messages.delete'),
95
+ ('admin', 'messages.update'),
96
+ ('admin', 'users.ban'),
97
+ ('moderator', 'messages.delete'),
98
+ ('moderator', 'messages.update');
99
+ ```
100
+
101
+ ### Passo 3: Custom Access Token Auth Hook function
102
+
103
+ A função `custom_access_token_hook(event jsonb) returns jsonb` roda **antes** do JWT ser issued — recebe o event (com `user_id` + claims atuais) e devolve event modificado.
104
+
105
+ ```sql
106
+ create or replace function public.custom_access_token_hook(event jsonb)
107
+ returns jsonb
108
+ language plpgsql
109
+ stable
110
+ as $$
111
+ declare
112
+ claims jsonb;
113
+ user_role public.app_role;
114
+ begin
115
+ -- buscar role do user em user_roles
116
+ select role into user_role
117
+ from public.user_roles
118
+ where user_id = (event->>'user_id')::uuid;
119
+
120
+ claims := event->'claims';
121
+
122
+ if user_role is not null then
123
+ claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
124
+ else
125
+ claims := jsonb_set(claims, '{user_role}', 'null');
126
+ end if;
127
+
128
+ -- atualiza objeto claims no event original
129
+ event := jsonb_set(event, '{claims}', claims);
130
+
131
+ return event;
132
+ end;
133
+ $$;
134
+ ```
135
+
136
+ **Caveat para multi-role:** se user tem múltiplos roles em `user_roles`, esta versão pega o primeiro (`select role into user_role`). Para múltiplos roles no JWT, use:
137
+
138
+ ```sql
139
+ -- variante multi-role: claim user_roles como array
140
+ select array_agg(role) into user_roles_array
141
+ from public.user_roles
142
+ where user_id = (event->>'user_id')::uuid;
143
+ -- ...
144
+ claims := jsonb_set(claims, '{user_roles}', to_jsonb(user_roles_array));
145
+ ```
146
+
147
+ ### Passo 4: Permissions canônicos para `supabase_auth_admin`
148
+
149
+ `supabase_auth_admin` é o Postgres role usado pelo Supabase Auth service ao invocar o hook. Precisa de permissions específicos:
150
+
151
+ ```sql
152
+ -- 1. acesso ao schema public
153
+ grant usage on schema public to supabase_auth_admin;
154
+
155
+ -- 2. permissão de EXECUTE no hook function (apenas auth_admin pode invocar)
156
+ grant execute
157
+ on function public.custom_access_token_hook
158
+ to supabase_auth_admin;
159
+
160
+ -- 3. REVOKE EXECUTE de roles públicos (cliente NÃO pode chamar diretamente)
161
+ revoke execute
162
+ on function public.custom_access_token_hook
163
+ from authenticated, anon, public;
164
+
165
+ -- 4. acesso à tabela user_roles
166
+ grant all
167
+ on table public.user_roles
168
+ to supabase_auth_admin;
169
+
170
+ -- 5. REVOKE acesso público à user_roles (cliente NÃO pode mutar roles)
171
+ revoke all
172
+ on table public.user_roles
173
+ from authenticated, anon, public;
174
+
175
+ -- 6. RLS policy permitindo supabase_auth_admin ler user_roles
176
+ create policy "Allow auth admin to read user roles" on public.user_roles
177
+ as permissive for select
178
+ to supabase_auth_admin
179
+ using (true);
180
+ ```
181
+
182
+ **Por quê REVOKE EXECUTE do hook:** sem isso, qualquer cliente autenticado poderia chamar `custom_access_token_hook(...)` e potencialmente abusar.
183
+
184
+ **Por quê REVOKE ALL FROM authenticated em user_roles:** roles são metadado privilegiado — apenas admins (service_role) podem inserir/atualizar. Cliente não deve ter SELECT direto na tabela (consultar via claim do JWT).
185
+
186
+ ### Passo 5: Habilitar o hook
187
+
188
+ **Dashboard (production):**
189
+
190
+ 1. Acesse `Authentication > Hooks (Beta)` no Dashboard Supabase
191
+ 2. Selecione "Custom Access Token" hook type
192
+ 3. Dropdown: selecione `public.custom_access_token_hook` (função criada no Passo 3)
193
+ 4. Save
194
+
195
+ **Local development:**
196
+
197
+ Em `supabase/config.toml`:
198
+
199
+ ```toml
200
+ [auth.hook.custom_access_token]
201
+ enabled = true
202
+ uri = "pg-functions://postgres/public/custom_access_token_hook"
203
+ ```
204
+
205
+ Reinicie o Supabase local (`supabase stop && supabase start`).
206
+
207
+ ### Passo 6: `authorize()` function
208
+
209
+ A função `authorize(permission app_permission) returns boolean` é o coração do RBAC — lê `user_role` do JWT e checa se o role tem a permission.
210
+
211
+ ```sql
212
+ create or replace function public.authorize(
213
+ requested_permission app_permission
214
+ )
215
+ returns boolean
216
+ language plpgsql
217
+ stable
218
+ security definer
219
+ set search_path = ''
220
+ as $$
221
+ declare
222
+ bind_permissions int;
223
+ user_role public.app_role;
224
+ begin
225
+ -- ler user_role do JWT (delivered via auth hook do Passo 3)
226
+ select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
227
+
228
+ -- contar permissions matching
229
+ select count(*)
230
+ into bind_permissions
231
+ from public.role_permissions
232
+ where role_permissions.permission = requested_permission
233
+ and role_permissions.role = user_role;
234
+
235
+ return bind_permissions > 0;
236
+ end;
237
+ $$;
238
+ ```
239
+
240
+ **Decisões canônicas:**
241
+
242
+ - `stable` — função consulta DB mas não muta; resultado é estável dentro de uma transação (Postgres pode cachear)
243
+ - `security definer` — roda com privilégios do owner (geralmente `postgres`) — necessário para acessar role_permissions sem RLS recursivo
244
+ - `set search_path = ''` — anti schema injection (cross-ref skill `supabase-database-functions`)
245
+
246
+ ### Passo 7: RLS policies usando authorize()
247
+
248
+ Em vez de hard-code role em policy, use `authorize(permission)`:
249
+
250
+ ```sql
251
+ -- ANTES (anti-pattern): hard-code role direto na policy
252
+ create policy "Allow admin delete channels" on public.channels for delete
253
+ to authenticated
254
+ using ((auth.jwt() ->> 'user_role') = 'admin');
255
+
256
+ -- DEPOIS (canônico v1.25): authorize() abstrai role → permission
257
+ create policy "Allow authorized delete access" on public.channels for delete
258
+ to authenticated
259
+ using ((SELECT authorize('channels.delete')));
260
+
261
+ create policy "Allow authorized delete access" on public.messages for delete
262
+ to authenticated
263
+ using ((SELECT authorize('messages.delete')));
264
+ ```
265
+
266
+ **Vantagens canônicas:**
267
+
268
+ - Adicionar nova permission = INSERT em role_permissions (sem alterar policy)
269
+ - Mudar quem tem permission = UPDATE em role_permissions
270
+ - Policies ficam estáveis; matriz permissions evolui
271
+ - Reutilizável em N policies
272
+
273
+ **Por que `(SELECT authorize(...))` com wrapper:** caching (cross-ref REGRA #2 da skill `supabase-rls-policies` v1.23) — função roda 1 vez por query, não 1 vez por linha.
274
+
275
+ ## Client-side — acessar custom claims
276
+
277
+ Auth hook só modifica o **access_token JWT**, não a auth response. Para acessar custom claims no cliente, decode o JWT:
278
+
279
+ ```bash
280
+ npm install jwt-decode
281
+ ```
282
+
283
+ ```js
284
+ import { jwtDecode } from 'jwt-decode'
285
+ import { createClient } from '@supabase/supabase-js'
286
+
287
+ const supabase = createClient(URL, ANON_KEY)
288
+
289
+ // listener para mudanças de auth state (login, logout, refresh)
290
+ supabase.auth.onAuthStateChange(async (event, session) => {
291
+ if (session) {
292
+ const jwt = jwtDecode(session.access_token)
293
+ const userRole = jwt.user_role // 'admin' | 'moderator' | 'user' | null
294
+
295
+ // usar userRole para UI conditional rendering
296
+ if (userRole === 'admin') {
297
+ // show admin panel
298
+ }
299
+ }
300
+ })
301
+ ```
302
+
303
+ **Para Next.js v16 + SSR:** decode o JWT no server-side via middleware ou Server Component:
304
+
305
+ ```ts
306
+ // app/middleware.ts ou Server Component
307
+ import { jwtDecode } from 'jwt-decode'
308
+
309
+ const { data: { session } } = await supabase.auth.getSession()
310
+ if (session) {
311
+ const jwt = jwtDecode(session.access_token)
312
+ const userRole = jwt.user_role
313
+ // ...
314
+ }
315
+ ```
316
+
317
+ **Para backend (Node.js/Python/etc):** use bibliotecas equivalentes:
318
+
319
+ - Node.js: `jsonwebtoken`, `express-jwt`, `koa-jwt`
320
+ - Python: `PyJWT`
321
+ - Dart: `dart_jsonwebtoken`
322
+ - .NET: `Microsoft.AspNetCore.Authentication.JwtBearer`
323
+
324
+ ## ⚠ Caveat — JWT Freshness
325
+
326
+ Mudanças em `user_roles` **NÃO** refletem imediatamente no JWT do user — apenas após **token refresh** (default TTL 1h em Supabase).
327
+
328
+ **Cenários:**
329
+
330
+ | Situação | Comportamento |
331
+ |----------|---------------|
332
+ | Admin adiciona role moderator ao user X | User X continua com `user_role: null` até próximo refresh (até 1h) |
333
+ | Admin remove role admin do user Y | User Y continua com `user_role: admin` no JWT atual; após refresh, policies bloqueiam |
334
+ | Permission adicionada em role_permissions (admin → users.ban) | Reflete IMEDIATAMENTE — authorize() consulta DB cada query (stable mas re-executa em query nova) |
335
+
336
+ **Implicações:**
337
+
338
+ - Adicionar role: aceitável demora (user pode logout/login para forçar refresh)
339
+ - **Remover role: PROBLEMA** — user comprometido pode usar JWT antigo até expirar. Para invalidação imediata:
340
+
341
+ ```ts
342
+ // force logout via admin API (server-side com service_role)
343
+ await supabase.auth.admin.signOut(userId)
344
+ // próximo request do user vai falhar; user precisa login novamente
345
+ ```
346
+
347
+ - Mudança em permissions matrix (role_permissions): IMEDIATA — não precisa refresh
348
+
349
+ ## Anti-patterns
350
+
351
+ ### Anti-pattern 1: Esquecer GRANT EXECUTE ao supabase_auth_admin
352
+
353
+ **Errado:**
354
+ ```sql
355
+ create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ...;
356
+ -- esqueceu o GRANT EXECUTE TO supabase_auth_admin
357
+ ```
358
+
359
+ **Por quê:** auth hook falha silenciosamente — JWT é issued mas SEM o claim `user_role`. Difícil de debug.
360
+
361
+ **Certo:** sempre incluir os 6 GRANTs/REVOKEs do Passo 4.
362
+
363
+ ### Anti-pattern 2: Hardcode role em policy ao invés de authorize()
364
+
365
+ **Errado:**
366
+ ```sql
367
+ create policy "admin_delete" on public.channels for delete to authenticated
368
+ using ((auth.jwt() ->> 'user_role') = 'admin');
369
+ ```
370
+
371
+ **Por quê:** acoplamento policy ↔ role. Mudar quem pode deletar requer ALTER POLICY (DDL); com authorize(), basta UPDATE em role_permissions.
372
+
373
+ **Certo:** `using ((SELECT authorize('channels.delete')))` — policy estável, matriz evolui.
374
+
375
+ ### Anti-pattern 3: Assumir JWT fresh sem invalidação
376
+
377
+ **Errado:**
378
+ ```ts
379
+ // admin remove role admin do user X
380
+ await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
381
+ // CRÍTICO: user X ainda tem JWT antigo válido por até 1h
382
+ ```
383
+
384
+ **Por quê:** revogação de role não é imediata. Em casos de comprometimento, atacante usa o JWT antigo para abusar de privilégios admin.
385
+
386
+ **Certo:** sempre force logout após mudança crítica de role:
387
+ ```ts
388
+ await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
389
+ await supabase.auth.admin.signOut(xId) // força refresh imediato
390
+ ```
391
+
392
+ ### Anti-pattern 4: Mutar app_metadata do cliente para mudar role
393
+
394
+ **Errado:**
395
+ ```ts
396
+ // no cliente
397
+ await supabase.auth.updateUser({ data: { role: 'admin' } })
398
+ // user_metadata é editável pelo cliente — privilege escalation
399
+ ```
400
+
401
+ **Por quê:** mesmo anti-pattern de `user_metadata` da skill `supabase-rls-policies` (REGRA #1). Pior — confunde "role via custom claim" com "role via user_metadata".
402
+
403
+ **Certo:** roles são mutados APENAS via INSERT em `public.user_roles` por backend (service_role) ou admin API. Cliente nunca toca em metadata de role.
404
+
405
+ ### Anti-pattern 5: Auth hook que faz query custosa
406
+
407
+ **Errado:**
408
+ ```sql
409
+ create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ... as $$
410
+ begin
411
+ -- query custosa em cada token issuance
412
+ select * into user_data from public.users
413
+ join public.organizations o on u.org_id = o.id
414
+ join public.subscriptions s on o.id = s.org_id
415
+ where u.id = (event->>'user_id')::uuid;
416
+ -- ...
417
+ end;
418
+ $$;
419
+ ```
420
+
421
+ **Por quê:** hook roda em **cada login + cada refresh**. Query lenta degrada latência de auth. Bom hook é < 10ms.
422
+
423
+ **Certo:** mantenha hook simples — query single table (user_roles), aggregate se necessário, sem JOINs. Se precisa de info complexa, materialize em coluna de user_roles (denormalize).
424
+
425
+ ## Postgres Roles vs Custom Claims — distinção canônica (v1.26)
426
+
427
+ **Custom Claims** (v1.25) e **Postgres Roles** (v1.26) são conceitos **complementares**, não substitutos:
428
+
429
+ | | Custom Claims (v1.25) | Postgres Roles (v1.26) |
430
+ |---|---|---|
431
+ | Tipo de access | Application access (end-users) | System access (service accounts) |
432
+ | Identidade | JWT claim `user_role` | Postgres login |
433
+ | Quem usa | End-users via PostgREST | Cron jobs, BI tools, ETL, admin scripts |
434
+ | Granularidade | Per-permission via `authorize()` | Per schema/table/function/column |
435
+ | Dinamicidade | Eventually consistent (TTL refresh) | Estática (alterações via SQL DDL) |
436
+ | Exemplo | "User admin pode deletar messages" | "Cron job pode SELECT em todas tabelas para backup" |
437
+
438
+ **Combine quando ambos aplicam:**
439
+
440
+ - End-user com role `admin` (via custom claim) acessa endpoint que invoca função `private.org_analytics()` com SECURITY DEFINER (que tem GRANT EXECUTE apenas para Postgres role `analytics_reader`)
441
+ - Service account `metabase_reader` (Postgres role) acessa view `public.user_active_tasks` com `security_invoker=true` (que respeita RLS via auth.jwt() — mas auth.jwt() é null para Postgres role login, então policies precisam considerar service_role bypass)
442
+
443
+ **NÃO use Postgres roles para application access:**
444
+
445
+ - ❌ Criar role Postgres `app_admin` para gerenciar quem é admin no app
446
+ - ❌ Criar role Postgres por end-user
447
+ - ✅ Use RLS + Custom Claims via auth hook (pattern v1.25)
448
+
449
+ **NÃO use Custom Claims para system access:**
450
+
451
+ - ❌ Service account cron job autenticando via JWT customizado
452
+ - ❌ BI tool authenticando via auth hook
453
+ - ✅ Use Postgres role dedicado (pattern v1.26)
454
+
455
+ Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
456
+
457
+ ## Cross-suite integration (v1.25)
458
+
459
+ Esta skill é base para o agent novo `supabase-rbac-implementer` (Phase 139) — recebe spec (roles + permissions matrix) via `Task()` e materializa setup completo. Pattern de handoff cooperativo herdado de v1.23/v1.24.
460
+
461
+ Para multi-tenant com role por org, **combine** custom claim (role global) + helper function PG (`private.has_role(role, org_id)` em RLS hierárquica — skill `multi-tenant-rls-hierarchy` v1.21). Custom claim sozinho não cobre context-aware multi-tenant.
462
+
463
+ ## Ver também
464
+
465
+ - [supabase-rbac-implementer](../../agents/supabase-rbac-implementer.md) (v1.25) — canonical materializer
466
+ - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) (v1.23) — section "RBAC via Custom Claims + authorize() function (v1.25)"
467
+ - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) (v1.24) — Camada 9 (Auth Hooks - Custom Claims) v1.25
468
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — pattern SECURITY DEFINER + supabase_auth_admin grants
469
+ - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — Next.js v16 + onAuthStateChange + jwt-decode integration
470
+ - [supabase-rls-hardener](../../agents/supabase-rls-hardener.md) (v1.23) — Detector 9 valida auth hook instalado (v1.25)
471
+ - [rbac-permissions-matrix-supabase](../rbac-permissions-matrix-supabase/SKILL.md) (v1.21) — alternativa via helper function STABLE (vs custom claim v1.25)
472
+ - [glossário compartilhado](../_shared-supabase/glossary.md) — termos custom claims, Custom Access Token Auth Hook, JWT user_role claim, authorize() function, supabase_auth_admin role, app_role enum, app_permission enum, jwt-decode client pattern