@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,635 +1,635 @@
1
- ---
2
- name: supabase-rls-policies
3
- description: Use ao criar/auditar RLS — sempre (select auth.uid()), policies separadas por operação, GRANTs antes de ENABLE RLS, IS NOT NULL para anti silent-fail, índices nas colunas, NUNCA user_meta…
4
- ---
5
-
6
- # Supabase — RLS Policies
7
-
8
- ## Quando usar
9
-
10
- LLM carrega esta skill quando criar, auditar ou debugar Row Level Security em Supabase. Trigger phrases:
11
-
12
- - "criar policy RLS", "RLS policy", "row level security"
13
- - "policies separadas por operação"
14
- - "auth.uid()", "auth.jwt()"
15
- - "MFA enforcement", "AAL2"
16
- - "auditar segurança de tabela Supabase"
17
- - "GRANT antes de ENABLE RLS", "defense in depth", "security_invoker"
18
- - "anon role vs anonymous user", "raw_app_meta_data vs raw_user_meta_data"
19
-
20
- ## Defense in depth — RLS como camada (v1.23)
21
-
22
- RLS é um **Postgres primitive** que oferece **defense in depth**: protege dados mesmo quando acessados por third-party tooling (Metabase, dbt, ferramentas BI conectadas via JDBC). Mesmo se um vazamento de chave API ou bypass na camada de aplicação acontece, RLS impede acesso indevido **no banco**.
23
-
24
- A regra mestre:
25
-
26
- > **RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.**
27
-
28
- Se você não tem RLS na camada do banco, você está confiando que **todo cliente** (front-end, backend, third-party, scripts, MCP tools) faça filtering corretamente. Isso é frágil.
29
-
30
- **Princípio em v1.23 (handoff cooperativo):** todo SQL gerado pelo kit passa pelo `supabase-rls-hardener` antes do output final — drafts upstream são preservados, mas hardening RLS é obrigatório.
31
-
32
- ## Regras absolutas
33
-
34
- **WARNING — REGRA #1 (segurança crítica):** **NUNCA** referencie `user_metadata` em policy de autorização. `user_metadata` é editável pelo cliente via `auth.updateUser({data: {...}})` — usuário pode auto-elevar `role: 'admin'` ou `plan: 'premium'`. Use **`app_metadata`** (set apenas via service_role) para roles/permissions. Splinter linter 0015 detecta automaticamente.
35
-
36
- **REGRA #2 (performance crítica):** **SEMPRE** envolva `auth.uid()` em `(select auth.uid())`. Sem o wrapper, Postgres reavalia a função **uma vez por linha** — degrada queries com filtro RLS em **até 1000×**. Documentado nos benchmarks oficiais (`test2a-wrappedSQL-uid`: 179ms → 9ms, 94.97% improvement).
37
-
38
- **REGRA #3 (anti silent-fail anônimo — v1.23):** Para policies que dependem de identidade autenticada, use **`auth.uid() IS NOT NULL AND auth.uid() = user_id`** ao invés de apenas `(select auth.uid()) = user_id`. Quando o usuário não está logado, `auth.uid()` retorna `null`, e `null = user_id` é **sempre false** silenciosamente — a policy "funciona" mas confunde debugging. O check explícito de `IS NOT NULL` deixa intent claro.
39
-
40
- **Outras regras:**
41
-
42
- - **`GRANT` antes de `ENABLE RLS`** (v1.23) — sempre conceda privilégios necessários aos roles `anon`/`authenticated`/`service_role` ANTES de habilitar RLS. Sem GRANT, mesmo policies "permissive" falham porque o role não tem permissão de tabela.
43
- - **`policies separadas por operação`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all` cobrindo CRUD inteiro.
44
- - **`TO authenticated`** ou **`to anon`** sempre explícito — nunca deixar implícito (default `to public` é insecure; impede otimização do executor que skipa execução de policy para roles fora do TO clause).
45
- - `for select` e `for delete` usam **apenas `using`** (sem `with check`).
46
- - `for insert` usa **apenas `with check`** (sem `using`).
47
- - `for update` usa **`using` + `with check`** (using para qual linha pode ser atualizada, with check para qual estado a linha pode assumir).
48
- - Índice obrigatório nas colunas referenciadas pela policy: `create index on public.tasks (user_id);`. Sem index, scan full em cada query.
49
- - `permissive` é default e preferido. `restrictive` é raro e exige justificativa explícita.
50
- - Para MFA enforcement: `(auth.jwt()->>'aal')::text = 'aal2'` em policies que exigem 2FA ativo.
51
- - **Views** com `security_invoker=true` (Postgres 15+) — por padrão views bypassam RLS (criadas como `security_definer` rodando como `postgres`). Ver seção "Views" abaixo.
52
-
53
- ## Setup canônico — GRANTs + ENABLE RLS (v1.23)
54
-
55
- O setup completo para uma tabela em schema exposto é:
56
-
57
- ```sql
58
- -- 1. GRANTs por role (faça ANTES de ENABLE RLS)
59
- grant select on public.tasks to anon;
60
- grant select, insert, update, delete on public.tasks to authenticated;
61
- grant select, insert, update, delete on public.tasks to service_role;
62
-
63
- -- 2. Enable RLS
64
- alter table public.tasks enable row level security;
65
-
66
- -- 3. Policies granulares (sem isso, nada é acessível com publishable key)
67
- create policy "users_select_own_tasks"
68
- on public.tasks for select to authenticated
69
- using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
70
-
71
- -- ... INSERT/UPDATE/DELETE policies análogos
72
-
73
- -- 4. Índice obrigatório
74
- create index tasks_user_id_idx on public.tasks (user_id);
75
- ```
76
-
77
- **Por que GRANTs antes de ENABLE RLS:** RLS é uma camada de filtragem de linhas **adicionada** sobre as permissões de tabela. Sem `GRANT SELECT TO authenticated`, mesmo se a policy retorna true, a query falha com "permission denied". Os GRANTs estabelecem o que o role *pode tentar* fazer; RLS estabelece *quais linhas* ele vê.
78
-
79
- **Roles canônicos Supabase:**
80
-
81
- - `anon` — requisição sem autenticação (usuário deslogado)
82
- - `authenticated` — requisição com JWT válido de Supabase Auth
83
- - `service_role` — bypassa RLS (ver "Bypassing RLS" abaixo); use APENAS em backend/admin
84
-
85
- ## Auto-enable RLS para tabelas novas (v1.23)
86
-
87
- Para evitar esquecimento humano, instale event trigger que ativa RLS automaticamente em `CREATE TABLE`:
88
-
89
- ```sql
90
- create or replace function rls_auto_enable()
91
- returns event_trigger
92
- language plpgsql
93
- security definer
94
- set search_path = pg_catalog
95
- as $$
96
- declare
97
- cmd record;
98
- begin
99
- for cmd in
100
- select *
101
- from pg_event_trigger_ddl_commands()
102
- where command_tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
103
- and object_type in ('table','partitioned table')
104
- loop
105
- if cmd.schema_name is not null and cmd.schema_name in ('public') and cmd.schema_name not in ('pg_catalog','information_schema') and cmd.schema_name not like 'pg_toast%' and cmd.schema_name not like 'pg_temp%' then
106
- begin
107
- execute format('alter table if exists %s enable row level security', cmd.object_identity);
108
- raise log 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
109
- exception
110
- when others then
111
- raise log 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
112
- end;
113
- else
114
- raise log 'rls_auto_enable: skip % (system schema or not in enforced list)', cmd.object_identity;
115
- end if;
116
- end loop;
117
- end;
118
- $$;
119
-
120
- drop event trigger if exists ensure_rls;
121
- create event trigger ensure_rls
122
- on ddl_command_end
123
- when tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
124
- execute function rls_auto_enable();
125
- ```
126
-
127
- **Caveats:**
128
- - Aplica-se a tabelas criadas **após** o trigger ser instalado — existentes precisam de `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` manual.
129
- - Tabelas em `pg_catalog`, `information_schema`, `pg_toast*`, `pg_temp*` são skipped (sistema).
130
- - Padrão completo + variações em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
131
-
132
- ## `anon` Postgres role vs anonymous Auth user (v1.23)
133
-
134
- Confusão comum: **`anon` Postgres role** ≠ **anonymous user de Supabase Auth**.
135
-
136
- - **`anon` Postgres role** — requisição sem JWT (usuário deslogado). É um Postgres role como qualquer outro; aparece em `TO anon` clauses.
137
- - **anonymous user de Supabase Auth** — usuário criado via `supabase.auth.signInAnonymously()` (Auth feature). Tem JWT válido, assume o role `authenticated`, e pode ser diferenciado por checar a claim `is_anonymous` no JWT.
138
-
139
- **Implicação em policies:**
140
-
141
- ```sql
142
- -- visivel a qualquer cliente (anon Postgres ou authenticated com is_anonymous=true ou regular)
143
- create policy "public_profiles_view"
144
- on public.profiles for select
145
- to authenticated, anon
146
- using (true);
147
-
148
- -- bloqueia anonymous Auth users mesmo que estejam autenticados
149
- create policy "premium_features_no_anonymous"
150
- on public.premium_data for select
151
- to authenticated
152
- using (
153
- (select auth.jwt()->>'is_anonymous')::boolean is not true
154
- and (select auth.uid()) = user_id
155
- );
156
- ```
157
-
158
- ## Patterns canônicos
159
-
160
- ### SELECT — usuário lê apenas suas próprias linhas
161
-
162
- ```sql
163
- -- política de SELECT com wrapper (select auth.uid()) + IS NOT NULL anti silent-fail
164
- create policy "users_select_own_tasks"
165
- on public.tasks
166
- for select
167
- to authenticated
168
- using (
169
- (select auth.uid()) is not null
170
- and (select auth.uid()) = user_id
171
- );
172
-
173
- -- index obrigatório (sem isso, scan full)
174
- create index tasks_user_id_idx on public.tasks (user_id);
175
- ```
176
-
177
- ### INSERT, UPDATE, DELETE separados
178
-
179
- ```sql
180
- -- INSERT — usuário só pode criar linhas com user_id = ele mesmo
181
- create policy "users_insert_own_tasks"
182
- on public.tasks
183
- for insert
184
- to authenticated
185
- with check (
186
- (select auth.uid()) is not null
187
- and (select auth.uid()) = user_id
188
- );
189
-
190
- -- UPDATE — restringe quais linhas (using) E qual estado novo (with check)
191
- -- IMPORTANTE: UPDATE também exige policy SELECT correspondente (sem ela, UPDATE não funciona)
192
- create policy "users_update_own_tasks"
193
- on public.tasks
194
- for update
195
- to authenticated
196
- using (
197
- (select auth.uid()) is not null
198
- and (select auth.uid()) = user_id
199
- )
200
- with check (
201
- (select auth.uid()) is not null
202
- and (select auth.uid()) = user_id
203
- );
204
-
205
- -- DELETE — apenas a coluna using (sem with check)
206
- create policy "users_delete_own_tasks"
207
- on public.tasks
208
- for delete
209
- to authenticated
210
- using (
211
- (select auth.uid()) is not null
212
- and (select auth.uid()) = user_id
213
- );
214
- ```
215
-
216
- ### Role admin via `app_metadata`
217
-
218
- ```sql
219
- -- segurança: app_metadata é set apenas via service_role (admin API)
220
- -- cliente NÃO pode mutá-lo
221
- create policy "admins_manage_all_tasks"
222
- on public.tasks
223
- for update
224
- to authenticated
225
- using (
226
- (select auth.jwt()->'app_metadata'->>'role') = 'admin'
227
- )
228
- with check (
229
- (select auth.jwt()->'app_metadata'->>'role') = 'admin'
230
- );
231
- ```
232
-
233
- ### Team membership via `app_metadata` (array)
234
-
235
- ```sql
236
- -- exemplo: app_metadata.teams = ["team_a", "team_b"]
237
- create policy "team_members_view"
238
- on public.team_resources
239
- for select
240
- to authenticated
241
- using (
242
- team_id::text in (select jsonb_array_elements_text((select auth.jwt()->'app_metadata'->'teams')))
243
- );
244
- ```
245
-
246
- ### MFA enforcement (AAL2)
247
-
248
- ```sql
249
- -- exigir 2FA ativo para acessar dados sensíveis
250
- -- restrictive force AND com policy SELECT base
251
- create policy "mfa_required_for_billing"
252
- on public.billing_records
253
- as restrictive
254
- for select
255
- to authenticated
256
- using (
257
- (select (auth.jwt()->>'aal')::text) = 'aal2'
258
- );
259
- ```
260
-
261
- ### Views com `security_invoker=true` (Postgres 15+) — v1.23
262
-
263
- Por padrão, views são criadas com `security_definer` (rodam com permissões do criador, geralmente `postgres`) — **bypassam RLS** das tabelas subjacentes. Em Postgres 15+, use `security_invoker=true` para fazer a view respeitar as policies RLS do role chamador:
264
-
265
- ```sql
266
- create view public.user_active_tasks
267
- with (security_invoker = true)
268
- as
269
- select id, title, status, created_at
270
- from public.tasks
271
- where status = 'active';
272
- ```
273
-
274
- **Em versões < Postgres 15:** revoke acesso de `anon`/`authenticated` à view, ou crie em schema não-exposto:
275
-
276
- ```sql
277
- -- alternativa pré-Postgres 15: revoke acesso
278
- revoke select on public.legacy_view from anon, authenticated;
279
-
280
- -- ou crie em schema privado:
281
- create view private.internal_view as ...;
282
- ```
283
-
284
- ## `app_metadata` vs `user_metadata` — caveats canônicos (v1.23)
285
-
286
- A função `auth.jwt()` retorna o JWT do usuário fazendo a requisição. Existem duas claims relacionadas a metadata:
287
-
288
- - **`raw_user_meta_data`** — pode ser atualizado pelo authenticated end-user via `supabase.auth.updateUser({ data: { ... } })`. **NÃO é seguro para authorization.** Use apenas para preferences (tema, idioma, avatar URL).
289
- - **`raw_app_meta_data`** — **NÃO pode ser atualizado pelo cliente** — só via service_role + admin API. **É o lugar correto** para roles, permissions, team memberships, plan tier.
290
-
291
- **Caveat #1 (JWT freshness):** JWT nem sempre está "fresh". Se você remove um user de um team e atualiza `app_metadata`, isso não reflete em `auth.jwt()` até o JWT ser refreshed (geralmente 1h TTL). Para invalidação imediata, force logout do user via `auth.admin.signOut()`.
292
-
293
- **Caveat #2 (cookie 4096 bytes):** Se você usa Cookies para Auth, esteja atento ao JWT size. Browsers limitam cada cookie a 4096 bytes. Se você embarca arrays grandes em `app_metadata.teams` ou similar, o JWT pode passar do limite. Mitigação: store apenas IDs, busque membership via SQL com policy/RPC.
294
-
295
- **Caveat #3 (NULL handling):** Para requests sem auth, `auth.uid()` retorna `null`. `null = user_id` é sempre **false silenciosamente** em SQL — a policy não erra, só não match. Sempre use `IS NOT NULL AND ...` (REGRA #3) para deixar intent claro.
296
-
297
- ## Performance — recomendações canônicas (v1.23)
298
-
299
- Toda authorization tem custo. RLS é poderoso mas pode ser caro em queries que scaneam muitas linhas. Recomendações baseadas em benchmarks oficiais Supabase:
300
-
301
- ### 1. Index nas colunas usadas em policies (99.94% improvement)
302
-
303
- ```sql
304
- -- antes: scan full
305
- -- depois: index scan
306
- create index tasks_user_id_idx on public.tasks (user_id);
307
- ```
308
-
309
- ### 2. Envolva funções em `(select ...)` para caching de plano (até 99.99% improvement)
310
-
311
- ```sql
312
- -- antes: re-executa auth.uid() por linha (179ms para 100k rows)
313
- using (auth.uid() = user_id)
314
-
315
- -- depois: initPlan, executa 1 vez e reusa (9ms)
316
- using ((select auth.uid()) = user_id)
317
- ```
318
-
319
- Aplicável a qualquer função que não muda baseado em row data: `auth.uid()`, `auth.jwt()`, `security definer` functions. **Não funciona** se o resultado depende da linha (ex: `is_owner(row_id)` precisa rodar por linha).
320
-
321
- ### 3. Adicione filtros redundantes nas queries client-side (94.74% improvement)
322
-
323
- Mesmo com policy aplicada, adicione `.eq()` ou `where` explícito no client:
324
-
325
- ```js
326
- // errado — confia 100% na policy
327
- const { data } = supabase.from('tasks').select()
328
-
329
- // certo — Postgres usa o filtro para construir query plan melhor
330
- const { data } = supabase.from('tasks').select().eq('user_id', userId)
331
- ```
332
-
333
- Policy é "implicit where clause"; filtro explícito ajuda o planner Postgres a escolher index scan ao invés de seq scan.
334
-
335
- ### 4. Specify role com `TO` clause (99.78% improvement)
336
-
337
- ```sql
338
- -- antes (sem TO): policy roda para todos roles, inclusive anon
339
- create policy "rls_test_select" on rls_test
340
- using ((select auth.uid()) = user_id);
341
-
342
- -- depois: anon skipa execução
343
- create policy "rls_test_select" on rls_test
344
- to authenticated
345
- using ((select auth.uid()) = user_id);
346
- ```
347
-
348
- ### 5. Use `security definer` functions para policies caras
349
-
350
- Se a policy precisa fazer JOIN ou query custosa, encapsule em função `security definer` que bypassa RLS interno:
351
-
352
- ```sql
353
- -- função pode acessar roles_table sem aplicar RLS recursivamente
354
- create function private.has_good_role()
355
- returns boolean
356
- language plpgsql
357
- security definer
358
- set search_path = ''
359
- as $$
360
- begin
361
- return exists (
362
- select 1 from public.roles_table
363
- where (select auth.uid()) = user_id and role = 'good_role'
364
- );
365
- end;
366
- $$;
367
-
368
- -- policy fica simples e cacheável
369
- create policy "rls_test_select"
370
- on public.test_table
371
- to authenticated
372
- using ((select private.has_good_role()));
373
- ```
374
-
375
- **IMPORTANTE:** funções `security definer` NUNCA em schema exposto (`public`). Sempre em `private` ou similar (não exposto via API settings).
376
-
377
- ### 6. Minimize joins (99.78% improvement)
378
-
379
- Reescreva policies que fazem join na source table para usar `IN` ao invés:
380
-
381
- ```sql
382
- -- antes: join entre test_table (source) e team_user (target)
383
- create policy "rls_test_select" on public.test_table
384
- to authenticated
385
- using (
386
- (select auth.uid()) in (
387
- select user_id
388
- from public.team_user
389
- where team_user.team_id = team_id -- join com source!
390
- )
391
- );
392
-
393
- -- depois: filtra primeiro, depois IN
394
- create policy "rls_test_select" on public.test_table
395
- to authenticated
396
- using (
397
- team_id in (
398
- select team_id
399
- from public.team_user
400
- where user_id = (select auth.uid()) -- sem join
401
- )
402
- );
403
- ```
404
-
405
- Se a lista interna pode passar de 1000 items, considere abordagem com `security definer` function ao invés.
406
-
407
- ## Anti-patterns
408
-
409
- ### Anti-pattern 1: `auth.uid()` sem `(select)` wrapper
410
-
411
- **Errado:**
412
- ```sql
413
- create policy "users_select_own_tasks"
414
- on public.tasks
415
- for select
416
- to authenticated
417
- using (auth.uid() = user_id); -- sem (select) — re-executa por linha
418
- ```
419
-
420
- **Por quê:** Postgres reavalia `auth.uid()` para cada linha sendo testada. Em tabela com 100k linhas, isso é 100k chamadas. O `(select)` permite Postgres executar **uma vez** e reusar — degradação de até **1000×** sem o wrapper. Documentado em [RLS Performance](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv).
421
-
422
- **Certo:**
423
- ```sql
424
- using ((select auth.uid()) = user_id)
425
- ```
426
-
427
- ### Anti-pattern 2: `WARNING user_metadata` em autorização — privilege escalation
428
-
429
- **Errado:**
430
- ```sql
431
- create policy "admins_manage_all"
432
- on public.tasks
433
- for update
434
- to authenticated
435
- using (
436
- (auth.jwt()->'user_metadata'->>'role') = 'admin' -- editável pelo cliente!
437
- );
438
- ```
439
-
440
- **Por quê:** o cliente pode chamar `supabase.auth.updateUser({ data: { role: 'admin' } })` e instantaneamente ganhar privilégios de admin. `user_metadata` é projetado para preferences do usuário (tema, idioma), não para autorização. Documentado em [Splinter linter 0015](https://supabase.github.io/splinter/0015_rls_references_user_metadata/).
441
-
442
- **Certo:** ver "Role admin via `app_metadata`" acima — `app_metadata` requer service_role para mutar.
443
-
444
- ### Anti-pattern 3: `for all` em vez de policies granulares
445
-
446
- **Errado:**
447
- ```sql
448
- create policy "users_manage_own_tasks"
449
- on public.tasks
450
- for all -- cobre CRUD inteiro com mesma regra
451
- to authenticated
452
- using ((select auth.uid()) = user_id);
453
- ```
454
-
455
- **Por quê:** semântica de `for all` mistura `using` (que controla SELECT/UPDATE/DELETE) com `with check` (que controla INSERT/UPDATE), levando a confusão. Em UPDATE você pode querer regras diferentes para "qual linha tocar" vs "qual estado novo". Granularidade explícita previne erros sutis.
456
-
457
- **Certo:** ver pattern com 4 policies separadas acima (SELECT, INSERT, UPDATE, DELETE).
458
-
459
- ### Anti-pattern 4: Sem índice nas colunas da policy
460
-
461
- **Errado:**
462
- ```sql
463
- -- policy referencia user_id mas não há index
464
- create policy "users_select_own_tasks" on public.tasks
465
- for select to authenticated
466
- using ((select auth.uid()) = user_id);
467
-
468
- -- (esqueceu) create index on public.tasks (user_id);
469
- ```
470
-
471
- **Por quê:** cada query com filtro RLS força sequential scan. Em produção com 100k+ linhas, isso é lentidão crônica.
472
-
473
- **Certo:**
474
- ```sql
475
- create index tasks_user_id_idx on public.tasks (user_id);
476
- ```
477
-
478
- ### Anti-pattern 5: ENABLE RLS sem GRANT — query falha silenciosa (v1.23)
479
-
480
- **Errado:**
481
- ```sql
482
- alter table public.tasks enable row level security;
483
- -- esqueceu de grant select on public.tasks to authenticated;
484
- create policy "users_select" ... using (...);
485
- ```
486
-
487
- **Por quê:** RLS é camada *sobre* permissões de tabela. Sem GRANT, role não pode tentar acessar a tabela — query retorna "permission denied" mesmo se policy permitiria.
488
-
489
- **Certo:** sempre GRANT primeiro, depois ENABLE RLS, depois policies (ver "Setup canônico" acima).
490
-
491
- ### Anti-pattern 6: View sem `security_invoker=true` em Postgres 15+ — bypass de RLS (v1.23)
492
-
493
- **Errado:**
494
- ```sql
495
- -- view criada como postgres user — bypassa RLS de public.tasks
496
- create view public.user_tasks_view as
497
- select id, title from public.tasks where user_id = auth.uid();
498
- ```
499
-
500
- **Por quê:** views por default são `security_definer` (rodam com permissões do criador). Cliente acessando a view bypass RLS das tabelas underlying. Atacante consegue ler dados de outros users via SELECT na view.
501
-
502
- **Certo:**
503
- ```sql
504
- create view public.user_tasks_view
505
- with (security_invoker = true)
506
- as
507
- select id, title from public.tasks
508
- where user_id = (select auth.uid());
509
- ```
510
-
511
- ### Anti-pattern 7: `null = user_id` silent-fail (v1.23)
512
-
513
- **Errado:**
514
- ```sql
515
- using ((select auth.uid()) = user_id)
516
- -- se user não logado, auth.uid() é null
517
- -- null = user_id é always false → policy "funciona" mas confunde
518
- ```
519
-
520
- **Por quê:** intent ambíguo — você queria bloquear não-logados ou tratar como caso especial? `null = X` retornar false silenciosamente esconde o intent e dificulta debug.
521
-
522
- **Certo:**
523
- ```sql
524
- using (
525
- (select auth.uid()) is not null
526
- and (select auth.uid()) = user_id
527
- )
528
- ```
529
-
530
- ## Postgres Roles vs RLS — quando usar qual (v1.26)
531
-
532
- Postgres roles e RLS são **conceitos complementares**:
533
-
534
- | | Postgres Roles | RLS |
535
- |---|---|---|
536
- | Escopo | System access (service accounts, cron, BI) | Application access (end-users) |
537
- | Identidade | Login Postgres (`SET ROLE`) | JWT (`auth.uid()`) |
538
- | Granularidade | Per schema/table/function | Per linha + coluna |
539
- | Audit | pg_stat_statements por role | RLS denial logs |
540
- | Use case | "Cron job pode SELECT em todas tabs" | "User vê apenas próprias rows" |
541
-
542
- **Princípio canônico v1.26:**
543
-
544
- - **Para end-users:** RLS + Custom Claims (v1.25) — NÃO criar role Postgres por user
545
- - **Para service accounts:** Postgres roles dedicados (NÃO usar service_role API key sempre)
546
- - **Para column-level access:** Postgres roles + column-level GRANTs (skill `supabase-column-level-security` v1.24)
547
-
548
- Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
549
-
550
- ## RBAC via Custom Claims + authorize() function (v1.25)
551
-
552
- A partir de v1.25, o pattern **canônico** de RBAC em Supabase é via **Custom Access Token Auth Hook** que injeta `user_role` no JWT durante geração do token. Em vez de policies fazendo JOIN custoso em `user_roles` table, a policy lê o claim direto via `auth.jwt() ->> 'user_role'` (ou via `authorize()` function que abstrai role → permission lookup).
553
-
554
- ```sql
555
- -- Pattern v1.25 — RLS policy usando authorize() (claim do JWT consultado)
556
- create policy "Allow authorized delete access" on public.channels for delete
557
- to authenticated
558
- using ((SELECT authorize('channels.delete')));
559
-
560
- -- vs. pattern v1.21 — RLS policy usando helper function STABLE (JOIN em DB)
561
- create policy "Allow admin delete" on public.channels for delete
562
- to authenticated
563
- using ((SELECT private.has_role('admin')));
564
- ```
565
-
566
- **Vantagens canônicas do pattern v1.25:**
567
-
568
- - **Performance:** claim no JWT é zero-JOIN; helper function STABLE faz query em user_roles table
569
- - **Composability:** `authorize(permission)` abstrai role → permission; trocar quem tem permission = UPDATE em role_permissions (sem alterar policies)
570
- - **Type safety:** `app_permission` enum garante consistência cross-policy
571
-
572
- **Caveat JWT freshness:** mudanças em `user_roles` só refletem no JWT após refresh (TTL 1h). Para revogação imediata, force logout via `auth.admin.signOut(userId)`. Em multi-tenant complexo (role por org), **combine** custom claim (role global) + helper function PG (role context-aware) — claim sozinho não cobre per-org context.
573
-
574
- Padrão completo (7 passos + anti-patterns + caveats) em [`supabase-custom-claims-rbac`](../supabase-custom-claims-rbac/SKILL.md) (v1.25).
575
-
576
- ## Combining RLS with Column-Level Privileges (v1.24)
577
-
578
- RLS row-level e column-level privileges são **camadas complementares**:
579
-
580
- - **RLS** filtra **quais linhas** o role vê/modifica
581
- - **Column privileges** filtra **quais colunas** o role pode acessar dentro da linha
582
-
583
- Combinação canônica: RLS + column-level (Camada 8 de defense-in-depth, skill `supabase-rls-defense-in-depth` v1.24).
584
-
585
- ```sql
586
- -- 1. RLS row-level — user só vê próprias posts
587
- create policy "users_select_own_posts"
588
- on public.posts for select to authenticated
589
- using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
590
-
591
- -- 2. Column-level — mesmo nas próprias posts, user não vê coluna sensível (ex: admin_notes)
592
- revoke select on table public.posts from authenticated;
593
- grant select (id, user_id, title, content, created_at) on table public.posts to authenticated;
594
-
595
- -- 3. service_role / admin_role vê tudo (incluindo admin_notes)
596
- grant select on table public.posts to service_role;
597
-
598
- -- Cliente DEVE listar colunas explicitamente:
599
- -- ❌ supabase.from('posts').select() — FALHA (wildcard expansion → admin_notes)
600
- -- ✅ supabase.from('posts').select('id, user_id, title, content, created_at')
601
- ```
602
-
603
- **Quando combinar:**
604
-
605
- - Compliance LGPD/GDPR onde algumas colunas (PII) precisam restrição extra além do RLS
606
- - Audit log com payload sanitizado — RLS filtra por org, column priv filtra payload
607
- - Billing data — RLS filtra por owner, column priv filtra credit_card_token
608
-
609
- **Quando NÃO combinar:**
610
-
611
- - Caso comum (admin/user roles) — use dedicated role table (skill [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md)) ao invés
612
- - Tabelas sem PII real — overhead sem benefício
613
-
614
- **Caveat crítico:** com column privileges, **todo SELECT deve listar colunas explicitamente** — `SELECT *` falha. Atualize SDK calls + queries SQL ad-hoc + ferramentas BI conectadas.
615
-
616
- Padrão completo + 4 patterns canônicos em [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md) (v1.24).
617
-
618
- ## Bypassing RLS — quando e como
619
-
620
- 3 mecanismos canônicos para bypass de RLS:
621
-
622
- 1. **`service_role`** — chave Supabase com bypass automático. **NUNCA** exponha ao cliente. Use APENAS em backend (Edge Functions com env var `SUPABASE_SERVICE_ROLE_KEY`, scripts admin, migrations). Caveat: ao chamar SDK Supabase com service_role mas com `Authorization: Bearer <user_jwt>` ainda set, RLS do user é aplicado (override).
623
- 2. **`alter role <name> with bypassrls`** — privilégio Postgres que permite role bypass RLS sempre. Use para roles internos (`postgres`, custom admin role). NUNCA conceda a um role que recebe requisições de cliente.
624
- 3. **`security definer` functions** — função roda com permissões do criador (geralmente `postgres` = bypassrls). Encapsule lógica admin/cross-tenant em função `security definer` no schema `private`.
625
-
626
- Padrões avançados em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
627
-
628
- ## Ver também
629
-
630
- - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) — event trigger, BYPASSRLS, service_role caveat, security definer, views security_invoker (v1.23)
631
- - [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` que respeitam RLS
632
- - [supabase-storage](../supabase-storage/SKILL.md) — RLS sobre `storage.objects` (multi-tenant path isolation)
633
- - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que popula `auth.uid()`
634
- - [supabase-migrations](../supabase-migrations/SKILL.md) — migrations sempre com GRANT + RLS habilitado em novas tabelas
635
- - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + roles + comandos CLI
1
+ ---
2
+ name: supabase-rls-policies
3
+ description: Use ao criar/auditar RLS — sempre (select auth.uid()), policies separadas por operação, GRANTs antes de ENABLE RLS, IS NOT NULL para anti silent-fail, índices nas colunas, NUNCA user_meta…
4
+ ---
5
+
6
+ # Supabase — RLS Policies
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando criar, auditar ou debugar Row Level Security em Supabase. Trigger phrases:
11
+
12
+ - "criar policy RLS", "RLS policy", "row level security"
13
+ - "policies separadas por operação"
14
+ - "auth.uid()", "auth.jwt()"
15
+ - "MFA enforcement", "AAL2"
16
+ - "auditar segurança de tabela Supabase"
17
+ - "GRANT antes de ENABLE RLS", "defense in depth", "security_invoker"
18
+ - "anon role vs anonymous user", "raw_app_meta_data vs raw_user_meta_data"
19
+
20
+ ## Defense in depth — RLS como camada (v1.23)
21
+
22
+ RLS é um **Postgres primitive** que oferece **defense in depth**: protege dados mesmo quando acessados por third-party tooling (Metabase, dbt, ferramentas BI conectadas via JDBC). Mesmo se um vazamento de chave API ou bypass na camada de aplicação acontece, RLS impede acesso indevido **no banco**.
23
+
24
+ A regra mestre:
25
+
26
+ > **RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.**
27
+
28
+ Se você não tem RLS na camada do banco, você está confiando que **todo cliente** (front-end, backend, third-party, scripts, MCP tools) faça filtering corretamente. Isso é frágil.
29
+
30
+ **Princípio em v1.23 (handoff cooperativo):** todo SQL gerado pelo kit passa pelo `supabase-rls-hardener` antes do output final — drafts upstream são preservados, mas hardening RLS é obrigatório.
31
+
32
+ ## Regras absolutas
33
+
34
+ **WARNING — REGRA #1 (segurança crítica):** **NUNCA** referencie `user_metadata` em policy de autorização. `user_metadata` é editável pelo cliente via `auth.updateUser({data: {...}})` — usuário pode auto-elevar `role: 'admin'` ou `plan: 'premium'`. Use **`app_metadata`** (set apenas via service_role) para roles/permissions. Splinter linter 0015 detecta automaticamente.
35
+
36
+ **REGRA #2 (performance crítica):** **SEMPRE** envolva `auth.uid()` em `(select auth.uid())`. Sem o wrapper, Postgres reavalia a função **uma vez por linha** — degrada queries com filtro RLS em **até 1000×**. Documentado nos benchmarks oficiais (`test2a-wrappedSQL-uid`: 179ms → 9ms, 94.97% improvement).
37
+
38
+ **REGRA #3 (anti silent-fail anônimo — v1.23):** Para policies que dependem de identidade autenticada, use **`auth.uid() IS NOT NULL AND auth.uid() = user_id`** ao invés de apenas `(select auth.uid()) = user_id`. Quando o usuário não está logado, `auth.uid()` retorna `null`, e `null = user_id` é **sempre false** silenciosamente — a policy "funciona" mas confunde debugging. O check explícito de `IS NOT NULL` deixa intent claro.
39
+
40
+ **Outras regras:**
41
+
42
+ - **`GRANT` antes de `ENABLE RLS`** (v1.23) — sempre conceda privilégios necessários aos roles `anon`/`authenticated`/`service_role` ANTES de habilitar RLS. Sem GRANT, mesmo policies "permissive" falham porque o role não tem permissão de tabela.
43
+ - **`policies separadas por operação`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all` cobrindo CRUD inteiro.
44
+ - **`TO authenticated`** ou **`to anon`** sempre explícito — nunca deixar implícito (default `to public` é insecure; impede otimização do executor que skipa execução de policy para roles fora do TO clause).
45
+ - `for select` e `for delete` usam **apenas `using`** (sem `with check`).
46
+ - `for insert` usa **apenas `with check`** (sem `using`).
47
+ - `for update` usa **`using` + `with check`** (using para qual linha pode ser atualizada, with check para qual estado a linha pode assumir).
48
+ - Índice obrigatório nas colunas referenciadas pela policy: `create index on public.tasks (user_id);`. Sem index, scan full em cada query.
49
+ - `permissive` é default e preferido. `restrictive` é raro e exige justificativa explícita.
50
+ - Para MFA enforcement: `(auth.jwt()->>'aal')::text = 'aal2'` em policies que exigem 2FA ativo.
51
+ - **Views** com `security_invoker=true` (Postgres 15+) — por padrão views bypassam RLS (criadas como `security_definer` rodando como `postgres`). Ver seção "Views" abaixo.
52
+
53
+ ## Setup canônico — GRANTs + ENABLE RLS (v1.23)
54
+
55
+ O setup completo para uma tabela em schema exposto é:
56
+
57
+ ```sql
58
+ -- 1. GRANTs por role (faça ANTES de ENABLE RLS)
59
+ grant select on public.tasks to anon;
60
+ grant select, insert, update, delete on public.tasks to authenticated;
61
+ grant select, insert, update, delete on public.tasks to service_role;
62
+
63
+ -- 2. Enable RLS
64
+ alter table public.tasks enable row level security;
65
+
66
+ -- 3. Policies granulares (sem isso, nada é acessível com publishable key)
67
+ create policy "users_select_own_tasks"
68
+ on public.tasks for select to authenticated
69
+ using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
70
+
71
+ -- ... INSERT/UPDATE/DELETE policies análogos
72
+
73
+ -- 4. Índice obrigatório
74
+ create index tasks_user_id_idx on public.tasks (user_id);
75
+ ```
76
+
77
+ **Por que GRANTs antes de ENABLE RLS:** RLS é uma camada de filtragem de linhas **adicionada** sobre as permissões de tabela. Sem `GRANT SELECT TO authenticated`, mesmo se a policy retorna true, a query falha com "permission denied". Os GRANTs estabelecem o que o role *pode tentar* fazer; RLS estabelece *quais linhas* ele vê.
78
+
79
+ **Roles canônicos Supabase:**
80
+
81
+ - `anon` — requisição sem autenticação (usuário deslogado)
82
+ - `authenticated` — requisição com JWT válido de Supabase Auth
83
+ - `service_role` — bypassa RLS (ver "Bypassing RLS" abaixo); use APENAS em backend/admin
84
+
85
+ ## Auto-enable RLS para tabelas novas (v1.23)
86
+
87
+ Para evitar esquecimento humano, instale event trigger que ativa RLS automaticamente em `CREATE TABLE`:
88
+
89
+ ```sql
90
+ create or replace function rls_auto_enable()
91
+ returns event_trigger
92
+ language plpgsql
93
+ security definer
94
+ set search_path = pg_catalog
95
+ as $$
96
+ declare
97
+ cmd record;
98
+ begin
99
+ for cmd in
100
+ select *
101
+ from pg_event_trigger_ddl_commands()
102
+ where command_tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
103
+ and object_type in ('table','partitioned table')
104
+ loop
105
+ if cmd.schema_name is not null and cmd.schema_name in ('public') and cmd.schema_name not in ('pg_catalog','information_schema') and cmd.schema_name not like 'pg_toast%' and cmd.schema_name not like 'pg_temp%' then
106
+ begin
107
+ execute format('alter table if exists %s enable row level security', cmd.object_identity);
108
+ raise log 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
109
+ exception
110
+ when others then
111
+ raise log 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
112
+ end;
113
+ else
114
+ raise log 'rls_auto_enable: skip % (system schema or not in enforced list)', cmd.object_identity;
115
+ end if;
116
+ end loop;
117
+ end;
118
+ $$;
119
+
120
+ drop event trigger if exists ensure_rls;
121
+ create event trigger ensure_rls
122
+ on ddl_command_end
123
+ when tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
124
+ execute function rls_auto_enable();
125
+ ```
126
+
127
+ **Caveats:**
128
+ - Aplica-se a tabelas criadas **após** o trigger ser instalado — existentes precisam de `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` manual.
129
+ - Tabelas em `pg_catalog`, `information_schema`, `pg_toast*`, `pg_temp*` são skipped (sistema).
130
+ - Padrão completo + variações em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
131
+
132
+ ## `anon` Postgres role vs anonymous Auth user (v1.23)
133
+
134
+ Confusão comum: **`anon` Postgres role** ≠ **anonymous user de Supabase Auth**.
135
+
136
+ - **`anon` Postgres role** — requisição sem JWT (usuário deslogado). É um Postgres role como qualquer outro; aparece em `TO anon` clauses.
137
+ - **anonymous user de Supabase Auth** — usuário criado via `supabase.auth.signInAnonymously()` (Auth feature). Tem JWT válido, assume o role `authenticated`, e pode ser diferenciado por checar a claim `is_anonymous` no JWT.
138
+
139
+ **Implicação em policies:**
140
+
141
+ ```sql
142
+ -- visivel a qualquer cliente (anon Postgres ou authenticated com is_anonymous=true ou regular)
143
+ create policy "public_profiles_view"
144
+ on public.profiles for select
145
+ to authenticated, anon
146
+ using (true);
147
+
148
+ -- bloqueia anonymous Auth users mesmo que estejam autenticados
149
+ create policy "premium_features_no_anonymous"
150
+ on public.premium_data for select
151
+ to authenticated
152
+ using (
153
+ (select auth.jwt()->>'is_anonymous')::boolean is not true
154
+ and (select auth.uid()) = user_id
155
+ );
156
+ ```
157
+
158
+ ## Patterns canônicos
159
+
160
+ ### SELECT — usuário lê apenas suas próprias linhas
161
+
162
+ ```sql
163
+ -- política de SELECT com wrapper (select auth.uid()) + IS NOT NULL anti silent-fail
164
+ create policy "users_select_own_tasks"
165
+ on public.tasks
166
+ for select
167
+ to authenticated
168
+ using (
169
+ (select auth.uid()) is not null
170
+ and (select auth.uid()) = user_id
171
+ );
172
+
173
+ -- index obrigatório (sem isso, scan full)
174
+ create index tasks_user_id_idx on public.tasks (user_id);
175
+ ```
176
+
177
+ ### INSERT, UPDATE, DELETE separados
178
+
179
+ ```sql
180
+ -- INSERT — usuário só pode criar linhas com user_id = ele mesmo
181
+ create policy "users_insert_own_tasks"
182
+ on public.tasks
183
+ for insert
184
+ to authenticated
185
+ with check (
186
+ (select auth.uid()) is not null
187
+ and (select auth.uid()) = user_id
188
+ );
189
+
190
+ -- UPDATE — restringe quais linhas (using) E qual estado novo (with check)
191
+ -- IMPORTANTE: UPDATE também exige policy SELECT correspondente (sem ela, UPDATE não funciona)
192
+ create policy "users_update_own_tasks"
193
+ on public.tasks
194
+ for update
195
+ to authenticated
196
+ using (
197
+ (select auth.uid()) is not null
198
+ and (select auth.uid()) = user_id
199
+ )
200
+ with check (
201
+ (select auth.uid()) is not null
202
+ and (select auth.uid()) = user_id
203
+ );
204
+
205
+ -- DELETE — apenas a coluna using (sem with check)
206
+ create policy "users_delete_own_tasks"
207
+ on public.tasks
208
+ for delete
209
+ to authenticated
210
+ using (
211
+ (select auth.uid()) is not null
212
+ and (select auth.uid()) = user_id
213
+ );
214
+ ```
215
+
216
+ ### Role admin via `app_metadata`
217
+
218
+ ```sql
219
+ -- segurança: app_metadata é set apenas via service_role (admin API)
220
+ -- cliente NÃO pode mutá-lo
221
+ create policy "admins_manage_all_tasks"
222
+ on public.tasks
223
+ for update
224
+ to authenticated
225
+ using (
226
+ (select auth.jwt()->'app_metadata'->>'role') = 'admin'
227
+ )
228
+ with check (
229
+ (select auth.jwt()->'app_metadata'->>'role') = 'admin'
230
+ );
231
+ ```
232
+
233
+ ### Team membership via `app_metadata` (array)
234
+
235
+ ```sql
236
+ -- exemplo: app_metadata.teams = ["team_a", "team_b"]
237
+ create policy "team_members_view"
238
+ on public.team_resources
239
+ for select
240
+ to authenticated
241
+ using (
242
+ team_id::text in (select jsonb_array_elements_text((select auth.jwt()->'app_metadata'->'teams')))
243
+ );
244
+ ```
245
+
246
+ ### MFA enforcement (AAL2)
247
+
248
+ ```sql
249
+ -- exigir 2FA ativo para acessar dados sensíveis
250
+ -- restrictive force AND com policy SELECT base
251
+ create policy "mfa_required_for_billing"
252
+ on public.billing_records
253
+ as restrictive
254
+ for select
255
+ to authenticated
256
+ using (
257
+ (select (auth.jwt()->>'aal')::text) = 'aal2'
258
+ );
259
+ ```
260
+
261
+ ### Views com `security_invoker=true` (Postgres 15+) — v1.23
262
+
263
+ Por padrão, views são criadas com `security_definer` (rodam com permissões do criador, geralmente `postgres`) — **bypassam RLS** das tabelas subjacentes. Em Postgres 15+, use `security_invoker=true` para fazer a view respeitar as policies RLS do role chamador:
264
+
265
+ ```sql
266
+ create view public.user_active_tasks
267
+ with (security_invoker = true)
268
+ as
269
+ select id, title, status, created_at
270
+ from public.tasks
271
+ where status = 'active';
272
+ ```
273
+
274
+ **Em versões < Postgres 15:** revoke acesso de `anon`/`authenticated` à view, ou crie em schema não-exposto:
275
+
276
+ ```sql
277
+ -- alternativa pré-Postgres 15: revoke acesso
278
+ revoke select on public.legacy_view from anon, authenticated;
279
+
280
+ -- ou crie em schema privado:
281
+ create view private.internal_view as ...;
282
+ ```
283
+
284
+ ## `app_metadata` vs `user_metadata` — caveats canônicos (v1.23)
285
+
286
+ A função `auth.jwt()` retorna o JWT do usuário fazendo a requisição. Existem duas claims relacionadas a metadata:
287
+
288
+ - **`raw_user_meta_data`** — pode ser atualizado pelo authenticated end-user via `supabase.auth.updateUser({ data: { ... } })`. **NÃO é seguro para authorization.** Use apenas para preferences (tema, idioma, avatar URL).
289
+ - **`raw_app_meta_data`** — **NÃO pode ser atualizado pelo cliente** — só via service_role + admin API. **É o lugar correto** para roles, permissions, team memberships, plan tier.
290
+
291
+ **Caveat #1 (JWT freshness):** JWT nem sempre está "fresh". Se você remove um user de um team e atualiza `app_metadata`, isso não reflete em `auth.jwt()` até o JWT ser refreshed (geralmente 1h TTL). Para invalidação imediata, force logout do user via `auth.admin.signOut()`.
292
+
293
+ **Caveat #2 (cookie 4096 bytes):** Se você usa Cookies para Auth, esteja atento ao JWT size. Browsers limitam cada cookie a 4096 bytes. Se você embarca arrays grandes em `app_metadata.teams` ou similar, o JWT pode passar do limite. Mitigação: store apenas IDs, busque membership via SQL com policy/RPC.
294
+
295
+ **Caveat #3 (NULL handling):** Para requests sem auth, `auth.uid()` retorna `null`. `null = user_id` é sempre **false silenciosamente** em SQL — a policy não erra, só não match. Sempre use `IS NOT NULL AND ...` (REGRA #3) para deixar intent claro.
296
+
297
+ ## Performance — recomendações canônicas (v1.23)
298
+
299
+ Toda authorization tem custo. RLS é poderoso mas pode ser caro em queries que scaneam muitas linhas. Recomendações baseadas em benchmarks oficiais Supabase:
300
+
301
+ ### 1. Index nas colunas usadas em policies (99.94% improvement)
302
+
303
+ ```sql
304
+ -- antes: scan full
305
+ -- depois: index scan
306
+ create index tasks_user_id_idx on public.tasks (user_id);
307
+ ```
308
+
309
+ ### 2. Envolva funções em `(select ...)` para caching de plano (até 99.99% improvement)
310
+
311
+ ```sql
312
+ -- antes: re-executa auth.uid() por linha (179ms para 100k rows)
313
+ using (auth.uid() = user_id)
314
+
315
+ -- depois: initPlan, executa 1 vez e reusa (9ms)
316
+ using ((select auth.uid()) = user_id)
317
+ ```
318
+
319
+ Aplicável a qualquer função que não muda baseado em row data: `auth.uid()`, `auth.jwt()`, `security definer` functions. **Não funciona** se o resultado depende da linha (ex: `is_owner(row_id)` precisa rodar por linha).
320
+
321
+ ### 3. Adicione filtros redundantes nas queries client-side (94.74% improvement)
322
+
323
+ Mesmo com policy aplicada, adicione `.eq()` ou `where` explícito no client:
324
+
325
+ ```js
326
+ // errado — confia 100% na policy
327
+ const { data } = supabase.from('tasks').select()
328
+
329
+ // certo — Postgres usa o filtro para construir query plan melhor
330
+ const { data } = supabase.from('tasks').select().eq('user_id', userId)
331
+ ```
332
+
333
+ Policy é "implicit where clause"; filtro explícito ajuda o planner Postgres a escolher index scan ao invés de seq scan.
334
+
335
+ ### 4. Specify role com `TO` clause (99.78% improvement)
336
+
337
+ ```sql
338
+ -- antes (sem TO): policy roda para todos roles, inclusive anon
339
+ create policy "rls_test_select" on rls_test
340
+ using ((select auth.uid()) = user_id);
341
+
342
+ -- depois: anon skipa execução
343
+ create policy "rls_test_select" on rls_test
344
+ to authenticated
345
+ using ((select auth.uid()) = user_id);
346
+ ```
347
+
348
+ ### 5. Use `security definer` functions para policies caras
349
+
350
+ Se a policy precisa fazer JOIN ou query custosa, encapsule em função `security definer` que bypassa RLS interno:
351
+
352
+ ```sql
353
+ -- função pode acessar roles_table sem aplicar RLS recursivamente
354
+ create function private.has_good_role()
355
+ returns boolean
356
+ language plpgsql
357
+ security definer
358
+ set search_path = ''
359
+ as $$
360
+ begin
361
+ return exists (
362
+ select 1 from public.roles_table
363
+ where (select auth.uid()) = user_id and role = 'good_role'
364
+ );
365
+ end;
366
+ $$;
367
+
368
+ -- policy fica simples e cacheável
369
+ create policy "rls_test_select"
370
+ on public.test_table
371
+ to authenticated
372
+ using ((select private.has_good_role()));
373
+ ```
374
+
375
+ **IMPORTANTE:** funções `security definer` NUNCA em schema exposto (`public`). Sempre em `private` ou similar (não exposto via API settings).
376
+
377
+ ### 6. Minimize joins (99.78% improvement)
378
+
379
+ Reescreva policies que fazem join na source table para usar `IN` ao invés:
380
+
381
+ ```sql
382
+ -- antes: join entre test_table (source) e team_user (target)
383
+ create policy "rls_test_select" on public.test_table
384
+ to authenticated
385
+ using (
386
+ (select auth.uid()) in (
387
+ select user_id
388
+ from public.team_user
389
+ where team_user.team_id = team_id -- join com source!
390
+ )
391
+ );
392
+
393
+ -- depois: filtra primeiro, depois IN
394
+ create policy "rls_test_select" on public.test_table
395
+ to authenticated
396
+ using (
397
+ team_id in (
398
+ select team_id
399
+ from public.team_user
400
+ where user_id = (select auth.uid()) -- sem join
401
+ )
402
+ );
403
+ ```
404
+
405
+ Se a lista interna pode passar de 1000 items, considere abordagem com `security definer` function ao invés.
406
+
407
+ ## Anti-patterns
408
+
409
+ ### Anti-pattern 1: `auth.uid()` sem `(select)` wrapper
410
+
411
+ **Errado:**
412
+ ```sql
413
+ create policy "users_select_own_tasks"
414
+ on public.tasks
415
+ for select
416
+ to authenticated
417
+ using (auth.uid() = user_id); -- sem (select) — re-executa por linha
418
+ ```
419
+
420
+ **Por quê:** Postgres reavalia `auth.uid()` para cada linha sendo testada. Em tabela com 100k linhas, isso é 100k chamadas. O `(select)` permite Postgres executar **uma vez** e reusar — degradação de até **1000×** sem o wrapper. Documentado em [RLS Performance](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv).
421
+
422
+ **Certo:**
423
+ ```sql
424
+ using ((select auth.uid()) = user_id)
425
+ ```
426
+
427
+ ### Anti-pattern 2: `WARNING user_metadata` em autorização — privilege escalation
428
+
429
+ **Errado:**
430
+ ```sql
431
+ create policy "admins_manage_all"
432
+ on public.tasks
433
+ for update
434
+ to authenticated
435
+ using (
436
+ (auth.jwt()->'user_metadata'->>'role') = 'admin' -- editável pelo cliente!
437
+ );
438
+ ```
439
+
440
+ **Por quê:** o cliente pode chamar `supabase.auth.updateUser({ data: { role: 'admin' } })` e instantaneamente ganhar privilégios de admin. `user_metadata` é projetado para preferences do usuário (tema, idioma), não para autorização. Documentado em [Splinter linter 0015](https://supabase.github.io/splinter/0015_rls_references_user_metadata/).
441
+
442
+ **Certo:** ver "Role admin via `app_metadata`" acima — `app_metadata` requer service_role para mutar.
443
+
444
+ ### Anti-pattern 3: `for all` em vez de policies granulares
445
+
446
+ **Errado:**
447
+ ```sql
448
+ create policy "users_manage_own_tasks"
449
+ on public.tasks
450
+ for all -- cobre CRUD inteiro com mesma regra
451
+ to authenticated
452
+ using ((select auth.uid()) = user_id);
453
+ ```
454
+
455
+ **Por quê:** semântica de `for all` mistura `using` (que controla SELECT/UPDATE/DELETE) com `with check` (que controla INSERT/UPDATE), levando a confusão. Em UPDATE você pode querer regras diferentes para "qual linha tocar" vs "qual estado novo". Granularidade explícita previne erros sutis.
456
+
457
+ **Certo:** ver pattern com 4 policies separadas acima (SELECT, INSERT, UPDATE, DELETE).
458
+
459
+ ### Anti-pattern 4: Sem índice nas colunas da policy
460
+
461
+ **Errado:**
462
+ ```sql
463
+ -- policy referencia user_id mas não há index
464
+ create policy "users_select_own_tasks" on public.tasks
465
+ for select to authenticated
466
+ using ((select auth.uid()) = user_id);
467
+
468
+ -- (esqueceu) create index on public.tasks (user_id);
469
+ ```
470
+
471
+ **Por quê:** cada query com filtro RLS força sequential scan. Em produção com 100k+ linhas, isso é lentidão crônica.
472
+
473
+ **Certo:**
474
+ ```sql
475
+ create index tasks_user_id_idx on public.tasks (user_id);
476
+ ```
477
+
478
+ ### Anti-pattern 5: ENABLE RLS sem GRANT — query falha silenciosa (v1.23)
479
+
480
+ **Errado:**
481
+ ```sql
482
+ alter table public.tasks enable row level security;
483
+ -- esqueceu de grant select on public.tasks to authenticated;
484
+ create policy "users_select" ... using (...);
485
+ ```
486
+
487
+ **Por quê:** RLS é camada *sobre* permissões de tabela. Sem GRANT, role não pode tentar acessar a tabela — query retorna "permission denied" mesmo se policy permitiria.
488
+
489
+ **Certo:** sempre GRANT primeiro, depois ENABLE RLS, depois policies (ver "Setup canônico" acima).
490
+
491
+ ### Anti-pattern 6: View sem `security_invoker=true` em Postgres 15+ — bypass de RLS (v1.23)
492
+
493
+ **Errado:**
494
+ ```sql
495
+ -- view criada como postgres user — bypassa RLS de public.tasks
496
+ create view public.user_tasks_view as
497
+ select id, title from public.tasks where user_id = auth.uid();
498
+ ```
499
+
500
+ **Por quê:** views por default são `security_definer` (rodam com permissões do criador). Cliente acessando a view bypass RLS das tabelas underlying. Atacante consegue ler dados de outros users via SELECT na view.
501
+
502
+ **Certo:**
503
+ ```sql
504
+ create view public.user_tasks_view
505
+ with (security_invoker = true)
506
+ as
507
+ select id, title from public.tasks
508
+ where user_id = (select auth.uid());
509
+ ```
510
+
511
+ ### Anti-pattern 7: `null = user_id` silent-fail (v1.23)
512
+
513
+ **Errado:**
514
+ ```sql
515
+ using ((select auth.uid()) = user_id)
516
+ -- se user não logado, auth.uid() é null
517
+ -- null = user_id é always false → policy "funciona" mas confunde
518
+ ```
519
+
520
+ **Por quê:** intent ambíguo — você queria bloquear não-logados ou tratar como caso especial? `null = X` retornar false silenciosamente esconde o intent e dificulta debug.
521
+
522
+ **Certo:**
523
+ ```sql
524
+ using (
525
+ (select auth.uid()) is not null
526
+ and (select auth.uid()) = user_id
527
+ )
528
+ ```
529
+
530
+ ## Postgres Roles vs RLS — quando usar qual (v1.26)
531
+
532
+ Postgres roles e RLS são **conceitos complementares**:
533
+
534
+ | | Postgres Roles | RLS |
535
+ |---|---|---|
536
+ | Escopo | System access (service accounts, cron, BI) | Application access (end-users) |
537
+ | Identidade | Login Postgres (`SET ROLE`) | JWT (`auth.uid()`) |
538
+ | Granularidade | Per schema/table/function | Per linha + coluna |
539
+ | Audit | pg_stat_statements por role | RLS denial logs |
540
+ | Use case | "Cron job pode SELECT em todas tabs" | "User vê apenas próprias rows" |
541
+
542
+ **Princípio canônico v1.26:**
543
+
544
+ - **Para end-users:** RLS + Custom Claims (v1.25) — NÃO criar role Postgres por user
545
+ - **Para service accounts:** Postgres roles dedicados (NÃO usar service_role API key sempre)
546
+ - **Para column-level access:** Postgres roles + column-level GRANTs (skill `supabase-column-level-security` v1.24)
547
+
548
+ Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
549
+
550
+ ## RBAC via Custom Claims + authorize() function (v1.25)
551
+
552
+ A partir de v1.25, o pattern **canônico** de RBAC em Supabase é via **Custom Access Token Auth Hook** que injeta `user_role` no JWT durante geração do token. Em vez de policies fazendo JOIN custoso em `user_roles` table, a policy lê o claim direto via `auth.jwt() ->> 'user_role'` (ou via `authorize()` function que abstrai role → permission lookup).
553
+
554
+ ```sql
555
+ -- Pattern v1.25 — RLS policy usando authorize() (claim do JWT consultado)
556
+ create policy "Allow authorized delete access" on public.channels for delete
557
+ to authenticated
558
+ using ((SELECT authorize('channels.delete')));
559
+
560
+ -- vs. pattern v1.21 — RLS policy usando helper function STABLE (JOIN em DB)
561
+ create policy "Allow admin delete" on public.channels for delete
562
+ to authenticated
563
+ using ((SELECT private.has_role('admin')));
564
+ ```
565
+
566
+ **Vantagens canônicas do pattern v1.25:**
567
+
568
+ - **Performance:** claim no JWT é zero-JOIN; helper function STABLE faz query em user_roles table
569
+ - **Composability:** `authorize(permission)` abstrai role → permission; trocar quem tem permission = UPDATE em role_permissions (sem alterar policies)
570
+ - **Type safety:** `app_permission` enum garante consistência cross-policy
571
+
572
+ **Caveat JWT freshness:** mudanças em `user_roles` só refletem no JWT após refresh (TTL 1h). Para revogação imediata, force logout via `auth.admin.signOut(userId)`. Em multi-tenant complexo (role por org), **combine** custom claim (role global) + helper function PG (role context-aware) — claim sozinho não cobre per-org context.
573
+
574
+ Padrão completo (7 passos + anti-patterns + caveats) em [`supabase-custom-claims-rbac`](../supabase-custom-claims-rbac/SKILL.md) (v1.25).
575
+
576
+ ## Combining RLS with Column-Level Privileges (v1.24)
577
+
578
+ RLS row-level e column-level privileges são **camadas complementares**:
579
+
580
+ - **RLS** filtra **quais linhas** o role vê/modifica
581
+ - **Column privileges** filtra **quais colunas** o role pode acessar dentro da linha
582
+
583
+ Combinação canônica: RLS + column-level (Camada 8 de defense-in-depth, skill `supabase-rls-defense-in-depth` v1.24).
584
+
585
+ ```sql
586
+ -- 1. RLS row-level — user só vê próprias posts
587
+ create policy "users_select_own_posts"
588
+ on public.posts for select to authenticated
589
+ using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
590
+
591
+ -- 2. Column-level — mesmo nas próprias posts, user não vê coluna sensível (ex: admin_notes)
592
+ revoke select on table public.posts from authenticated;
593
+ grant select (id, user_id, title, content, created_at) on table public.posts to authenticated;
594
+
595
+ -- 3. service_role / admin_role vê tudo (incluindo admin_notes)
596
+ grant select on table public.posts to service_role;
597
+
598
+ -- Cliente DEVE listar colunas explicitamente:
599
+ -- ❌ supabase.from('posts').select() — FALHA (wildcard expansion → admin_notes)
600
+ -- ✅ supabase.from('posts').select('id, user_id, title, content, created_at')
601
+ ```
602
+
603
+ **Quando combinar:**
604
+
605
+ - Compliance LGPD/GDPR onde algumas colunas (PII) precisam restrição extra além do RLS
606
+ - Audit log com payload sanitizado — RLS filtra por org, column priv filtra payload
607
+ - Billing data — RLS filtra por owner, column priv filtra credit_card_token
608
+
609
+ **Quando NÃO combinar:**
610
+
611
+ - Caso comum (admin/user roles) — use dedicated role table (skill [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md)) ao invés
612
+ - Tabelas sem PII real — overhead sem benefício
613
+
614
+ **Caveat crítico:** com column privileges, **todo SELECT deve listar colunas explicitamente** — `SELECT *` falha. Atualize SDK calls + queries SQL ad-hoc + ferramentas BI conectadas.
615
+
616
+ Padrão completo + 4 patterns canônicos em [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md) (v1.24).
617
+
618
+ ## Bypassing RLS — quando e como
619
+
620
+ 3 mecanismos canônicos para bypass de RLS:
621
+
622
+ 1. **`service_role`** — chave Supabase com bypass automático. **NUNCA** exponha ao cliente. Use APENAS em backend (Edge Functions com env var `SUPABASE_SERVICE_ROLE_KEY`, scripts admin, migrations). Caveat: ao chamar SDK Supabase com service_role mas com `Authorization: Bearer <user_jwt>` ainda set, RLS do user é aplicado (override).
623
+ 2. **`alter role <name> with bypassrls`** — privilégio Postgres que permite role bypass RLS sempre. Use para roles internos (`postgres`, custom admin role). NUNCA conceda a um role que recebe requisições de cliente.
624
+ 3. **`security definer` functions** — função roda com permissões do criador (geralmente `postgres` = bypassrls). Encapsule lógica admin/cross-tenant em função `security definer` no schema `private`.
625
+
626
+ Padrões avançados em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
627
+
628
+ ## Ver também
629
+
630
+ - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) — event trigger, BYPASSRLS, service_role caveat, security definer, views security_invoker (v1.23)
631
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` que respeitam RLS
632
+ - [supabase-storage](../supabase-storage/SKILL.md) — RLS sobre `storage.objects` (multi-tenant path isolation)
633
+ - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que popula `auth.uid()`
634
+ - [supabase-migrations](../supabase-migrations/SKILL.md) — migrations sempre com GRANT + RLS habilitado em novas tabelas
635
+ - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + roles + comandos CLI