@luanpdd/kit-mcp 1.7.0 → 1.8.1

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 (38) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/gates/agent-no-recursive-dispatch.md +48 -0
  3. package/gates/budget-description.md +68 -0
  4. package/gates/no-personal-uuid.md +72 -0
  5. package/gates/skill-must-include.md +69 -0
  6. package/gates/sync-idempotent.md +62 -0
  7. package/kit/agents/codebase-mapper.md +1 -1
  8. package/kit/agents/executor.md +17 -0
  9. package/kit/agents/planner.md +35 -0
  10. package/kit/agents/project-researcher.md +1 -1
  11. package/kit/agents/schema-checker.md +4 -4
  12. package/kit/agents/supabase-architect.md +153 -0
  13. package/kit/agents/supabase-auth-bootstrapper.md +298 -0
  14. package/kit/agents/supabase-edge-fn-writer.md +185 -0
  15. package/kit/agents/supabase-migration-writer.md +156 -0
  16. package/kit/agents/supabase-realtime-implementer.md +252 -0
  17. package/kit/agents/supabase-rls-writer.md +218 -0
  18. package/kit/agents/supabase-storage-implementer.md +240 -0
  19. package/kit/agents/user-profiler.md +1 -1
  20. package/kit/agents/verifier.md +1 -1
  21. package/kit/commands/depurar.md +17 -0
  22. package/kit/commands/fazer.md +15 -0
  23. package/kit/commands/supabase.md +148 -0
  24. package/kit/framework/workflows/discuss-phase.md +19 -0
  25. package/kit/framework/workflows/plan-phase.md +25 -0
  26. package/kit/skills/_shared-supabase/glossary.md +180 -0
  27. package/kit/skills/supabase-auth-ssr/SKILL.md +260 -0
  28. package/kit/skills/supabase-cron-queues/SKILL.md +266 -0
  29. package/kit/skills/supabase-database-functions/SKILL.md +247 -0
  30. package/kit/skills/supabase-declarative-schema/SKILL.md +183 -0
  31. package/kit/skills/supabase-edge-functions/SKILL.md +242 -0
  32. package/kit/skills/supabase-migrations/SKILL.md +175 -0
  33. package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -0
  34. package/kit/skills/supabase-postgres-style/SKILL.md +138 -0
  35. package/kit/skills/supabase-realtime/SKILL.md +236 -0
  36. package/kit/skills/supabase-rls-policies/SKILL.md +185 -0
  37. package/kit/skills/supabase-storage/SKILL.md +234 -0
  38. package/package.json +1 -1
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: supabase-postgres-style
3
+ description: Use ao escrever SQL para Postgres/Supabase — snake_case, lowercase reserved, plurais para tabelas e singular para colunas, ISO 8601, CTEs lineares.
4
+ ---
5
+
6
+ # Supabase — Postgres Style Guide
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando trabalhar com SQL em projeto Supabase/Postgres — definir schemas, escrever queries, criar tabelas/colunas, padronizar dates, decidir nomes. Trigger phrases:
11
+
12
+ - "criar tabela em postgres", "create table"
13
+ - "escrever query SQL para Supabase"
14
+ - "estilo de schema", "convenção de nomes em SQL"
15
+ - "estrutura de query complexa" (CTE vs subquery)
16
+
17
+ ## Regras absolutas
18
+
19
+ - **Sempre** use **`lowercase reserved`** words: `select`, `from`, `where`, `join`, `with`, `as`. **Nunca** `SELECT`, `FROM`, `WHERE` em maiúscula.
20
+ - **Sempre** use **`snake_case`** para tabelas, colunas, funções, índices. **Nunca** `camelCase` ou `PascalCase`.
21
+ - **Tabelas em plural** (`books`, `authors`, `users`); **colunas em singular** (`title`, `author_id`, `created_at`).
22
+ - **Datas em `ISO 8601`** com timezone: `timestamptz` (não `timestamp` sem tz). String literal: `'2026-05-06T12:00:00Z'`.
23
+ - Aliases descritivos com `as` **explícito**: `select b.title as book_title from books as b`. Nunca alias implícito.
24
+ - Evite `id` ambíguo. Em FKs use `<entity>_id` (`author_id`, `user_id`). Em PKs use `id` apenas se a tabela já é singular contextualmente.
25
+ - Para queries complexas: prefira **múltiplas CTEs lineares** sobre subqueries aninhadas. Cada CTE com 1 propósito + comentário.
26
+ - JOINs sempre com nomes completos da tabela qualificadora: `books.author_id = authors.id` (não aliases curtos como `b.x = a.y` sem `as`).
27
+
28
+ ## Patterns canônicos
29
+
30
+ ### Tabela típica
31
+
32
+ ```sql
33
+ -- estilo: lowercase reserved + snake_case + tabela em plural + colunas em singular
34
+ create table public.books (
35
+ id uuid primary key default gen_random_uuid(),
36
+ title text not null,
37
+ author_id uuid references public.authors (id) on delete cascade,
38
+ published_at timestamptz, -- ISO 8601 com timezone
39
+ created_at timestamptz not null default now(),
40
+ updated_at timestamptz not null default now()
41
+ );
42
+
43
+ -- comentário descritivo na tabela (até 1024 chars)
44
+ comment on table public.books is 'Catálogo de livros disponíveis na biblioteca.';
45
+ ```
46
+
47
+ ### Query simples (uma linha por cláusula)
48
+
49
+ ```sql
50
+ -- query curta: pode ficar em poucas linhas
51
+ select id, title, author_id
52
+ from public.books
53
+ where published_at is not null
54
+ order by published_at desc
55
+ limit 50;
56
+ ```
57
+
58
+ ### Query complexa com CTEs lineares
59
+
60
+ ```sql
61
+ -- preferir CTEs lineares — cada uma com 1 propósito
62
+ with recent_books as (
63
+ -- 1. livros publicados nos últimos 30 dias
64
+ select id, title, author_id, published_at
65
+ from public.books
66
+ where published_at >= now() - interval '30 days'
67
+ ),
68
+ author_stats as (
69
+ -- 2. agregação por autor sobre os livros recentes
70
+ select author_id, count(*) as total_recent
71
+ from recent_books
72
+ group by author_id
73
+ )
74
+ select a.name as author_name, s.total_recent
75
+ from author_stats as s
76
+ join public.authors as a on a.id = s.author_id
77
+ order by s.total_recent desc;
78
+ ```
79
+
80
+ ## Anti-patterns
81
+
82
+ ### Anti-pattern 1: Reserved words em maiúscula + mixed case
83
+
84
+ **Errado:**
85
+ ```sql
86
+ SELECT * FROM Books WHERE Title='X'
87
+ ```
88
+
89
+ **Por quê:** vai contra convenção da comunidade Postgres + dificulta diff em pull requests. Identificadores `Books` exigirão quoting (`"Books"`) sempre, ou o Postgres dobra para `books` quietly.
90
+
91
+ **Certo:**
92
+ ```sql
93
+ select * from books where title = 'X'
94
+ ```
95
+
96
+ ### Anti-pattern 2: `timestamp` sem timezone + camelCase
97
+
98
+ **Errado:**
99
+ ```sql
100
+ create table users (
101
+ id int primary key,
102
+ createdAt timestamp, -- sem timezone
103
+ fullName text -- camelCase
104
+ );
105
+ ```
106
+
107
+ **Por quê:** `timestamp` (sem `tz`) não preserva timezone — converte tudo para o timezone do servidor; ambíguo em apps multi-região. `camelCase` em SQL é estilizado por engine driver (caso por caso) e quebra em ferramentas que esperam snake_case.
108
+
109
+ **Certo:**
110
+ ```sql
111
+ create table users (
112
+ id uuid primary key default gen_random_uuid(),
113
+ created_at timestamptz not null default now(),
114
+ full_name text
115
+ );
116
+ ```
117
+
118
+ ### Anti-pattern 3: subqueries aninhadas em vez de CTEs
119
+
120
+ **Errado:**
121
+ ```sql
122
+ select * from (
123
+ select author_id, count(*) from (
124
+ select * from books where published_at > now() - interval '30 days'
125
+ ) recent group by author_id
126
+ ) ranked where count > 5;
127
+ ```
128
+
129
+ **Por quê:** ilegível, impossível de comentar cada nível, query plan harder to read.
130
+
131
+ **Certo:** ver "Query complexa com CTEs lineares" acima.
132
+
133
+ ## Ver também
134
+
135
+ - [supabase-migrations](../supabase-migrations/SKILL.md) — estilo aplicado em arquivos de migration
136
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — estilo aplicado em funções Postgres
137
+ - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — convenção de naming em policies
138
+ - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + comandos CLI canônicos
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: supabase-realtime
3
+ description: Use ao implementar Realtime — broadcast com private:true, naming scope:entity:id, RLS sobre realtime.messages, removeChannel cleanup, migrar de postgres_changes.
4
+ ---
5
+
6
+ # Supabase — Realtime
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando implementar features Realtime em Supabase (chat, presence, notifications, live dashboards). Trigger phrases:
11
+
12
+ - "Supabase Realtime", "broadcast", "presence"
13
+ - "subscrever a mudanças no banco em tempo real"
14
+ - "WebSocket Supabase"
15
+ - "migrar postgres_changes para broadcast"
16
+ - "RLS realtime.messages"
17
+ - "channel state", "removeChannel"
18
+
19
+ ## Regras absolutas
20
+
21
+ - **Use `broadcast` por default** — `postgres_changes` é pattern legado (single-threaded, não escala). **Migrar para broadcast** em features novas.
22
+ - **`private: true`** em todos os canais novos — exige autenticação + RLS sobre `realtime.messages`. Default em produção 2026.
23
+ - **Naming canônico `scope:entity:id`** — ex: `room:messages:abc123`, `user:notifications:xyz789`, `org:announcements:org_42`.
24
+ - **Eventos em `entity_action`** — ex: `message_inserted`, `task_updated`, `presence_joined`.
25
+ - **`removeChannel` no cleanup obrigatório** — chamar `supabase.removeChannel(channel)` em `useEffect return` ou equivalente. Sem cleanup, memory leak + stale state (anti-pitfall B1).
26
+ - **State checking antes de subscribe** — `if (channel.state === 'joined') return;` evita double-subscribe.
27
+ - **RLS sobre `realtime.messages`** — SELECT (read) e INSERT (write) policies separadas, com index nas colunas usadas.
28
+ - **Use Presence com moderação** — apenas para online status / cursors colaborativos, não para listas de objects (use queries normais).
29
+ - Realtime tem **retry built-in** — log `status` no callback do `subscribe` mas não implementar retry manual.
30
+
31
+ ## Patterns canônicos
32
+
33
+ ### Subscribe via broadcast — client com cleanup
34
+
35
+ ```ts
36
+ // PT-BR: subscrição típica em Client Component
37
+ 'use client'
38
+ import { useEffect, useState } from 'react'
39
+ import { createClient } from '@/utils/supabase/client'
40
+
41
+ export function ChatRoom({ roomId }: { roomId: string }) {
42
+ const supabase = createClient()
43
+ const [messages, setMessages] = useState<Message[]>([])
44
+
45
+ useEffect(() => {
46
+ const channel = supabase
47
+ .channel(`room:messages:${roomId}`, { config: { private: true } })
48
+ .on('broadcast', { event: 'message_inserted' }, ({ payload }) => {
49
+ setMessages((prev) => [...prev, payload as Message])
50
+ })
51
+ .subscribe((status) => {
52
+ if (status === 'SUBSCRIBED') console.log('joined channel')
53
+ if (status === 'CHANNEL_ERROR') console.error('channel error')
54
+ })
55
+
56
+ // PT-BR: cleanup obrigatório — sem isso, memory leak
57
+ return () => {
58
+ supabase.removeChannel(channel)
59
+ }
60
+ }, [roomId, supabase])
61
+
62
+ return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
63
+ }
64
+ ```
65
+
66
+ ### RLS sobre `realtime.messages`
67
+
68
+ ```sql
69
+ -- PT-BR: SELECT policy permite ouvir broadcast em canal autenticado
70
+ -- Granular: SELECT = read, INSERT = write — duas policies separadas
71
+ create policy "auth_select_realtime_messages"
72
+ on realtime.messages
73
+ for select
74
+ to authenticated
75
+ using ((select auth.uid()) is not null);
76
+
77
+ -- PT-BR: INSERT policy permite enviar broadcast
78
+ create policy "auth_insert_realtime_messages"
79
+ on realtime.messages
80
+ for insert
81
+ to authenticated
82
+ with check ((select auth.uid()) is not null);
83
+
84
+ -- PT-BR: index obrigatório (extension é a coluna usada por broadcast)
85
+ create index if not exists realtime_messages_extension_idx
86
+ on realtime.messages (extension);
87
+ ```
88
+
89
+ ### DB trigger via `realtime.broadcast_changes`
90
+
91
+ Para emitir broadcast quando linha de tabela muda (substitui `postgres_changes`):
92
+
93
+ ```sql
94
+ -- PT-BR: trigger function emite broadcast no canal scope:entity:id
95
+ create or replace function public.notify_message_insert()
96
+ returns trigger
97
+ language plpgsql
98
+ security invoker
99
+ set search_path = ''
100
+ as $$
101
+ begin
102
+ perform realtime.broadcast_changes(
103
+ 'room:messages:' || new.room_id::text, -- canal
104
+ 'message_inserted', -- event name
105
+ 'INSERT', -- operation
106
+ 'messages', -- table
107
+ 'public', -- schema
108
+ new, -- new row
109
+ null -- old row
110
+ );
111
+ return new;
112
+ end;
113
+ $$;
114
+
115
+ create trigger messages_broadcast_on_insert
116
+ after insert on public.messages
117
+ for each row
118
+ execute function public.notify_message_insert();
119
+ ```
120
+
121
+ ### Presence — apenas para online status
122
+
123
+ ```ts
124
+ // PT-BR: presence é sparingly — só para "quem está online"
125
+ const channel = supabase
126
+ .channel(`room:${roomId}`, { config: { private: true } })
127
+ .on('presence', { event: 'sync' }, () => {
128
+ const state = channel.presenceState()
129
+ setOnlineUsers(Object.keys(state))
130
+ })
131
+ .subscribe(async (status) => {
132
+ if (status !== 'SUBSCRIBED') return
133
+ await channel.track({ user_id: userId, online_at: new Date().toISOString() })
134
+ })
135
+
136
+ return () => {
137
+ supabase.removeChannel(channel)
138
+ }
139
+ ```
140
+
141
+ ### Migrar de `postgres_changes` para `broadcast`
142
+
143
+ ```ts
144
+ // ❌ PADRÃO LEGADO — postgres_changes
145
+ const channel = supabase
146
+ .channel('messages_changes')
147
+ .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, callback)
148
+ .subscribe()
149
+
150
+ // ✅ PADRÃO ATUAL — broadcast com trigger DB
151
+ // 1. Criar trigger SQL `realtime.broadcast_changes` (ver pattern acima)
152
+ // 2. Subscribe via broadcast no client:
153
+ const channel = supabase
154
+ .channel(`room:messages:${roomId}`, { config: { private: true } })
155
+ .on('broadcast', { event: 'message_inserted' }, callback)
156
+ .subscribe()
157
+ ```
158
+
159
+ ## Anti-patterns
160
+
161
+ ### Anti-pattern 1: Canal sem `private: true`
162
+
163
+ **Errado:**
164
+ ```ts
165
+ const channel = supabase.channel('messages') // canal público
166
+ .on('broadcast', { event: 'msg' }, callback)
167
+ .subscribe()
168
+ ```
169
+
170
+ **Por quê:** canal público — qualquer cliente recebe payload sem RLS. Em produção isso vaza dados (broadcast pode incluir info sensível).
171
+
172
+ **Certo:**
173
+ ```ts
174
+ const channel = supabase
175
+ .channel(`room:messages:${roomId}`, { config: { private: true } })
176
+ .on('broadcast', { event: 'message_inserted' }, callback)
177
+ .subscribe()
178
+ ```
179
+
180
+ ### Anti-pattern 2: Subscribe sem `removeChannel` no cleanup
181
+
182
+ **Errado:**
183
+ ```tsx
184
+ useEffect(() => {
185
+ const channel = supabase.channel('...').subscribe()
186
+ // ⚠ sem return — canal nunca limpo
187
+ }, [])
188
+ ```
189
+
190
+ **Por quê:** memory leak. Em SPA com navegação, canais antigos continuam recebendo eventos — UI fica em estado inconsistente. WebSocket connections crescem indefinidamente.
191
+
192
+ **Certo:**
193
+ ```tsx
194
+ useEffect(() => {
195
+ const channel = supabase.channel('...').subscribe()
196
+ return () => {
197
+ supabase.removeChannel(channel)
198
+ }
199
+ }, [])
200
+ ```
201
+
202
+ ### Anti-pattern 3: `postgres_changes` em features novas
203
+
204
+ **Errado:**
205
+ ```ts
206
+ supabase.channel('changes')
207
+ .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, callback)
208
+ .subscribe()
209
+ ```
210
+
211
+ **Por quê:** `postgres_changes` é single-threaded em Realtime backend. Em escala (>100 connections, >1k events/sec), throughput cai drasticamente. Documentado em [Realtime Limits](https://supabase.com/docs/guides/realtime/limits).
212
+
213
+ **Certo:** trigger DB com `realtime.broadcast_changes` + subscribe via `broadcast` (ver pattern "Migrar" acima).
214
+
215
+ ### Anti-pattern 4: Presence para listar objetos
216
+
217
+ **Errado:**
218
+ ```ts
219
+ // ⚠ usar presence para listar tasks ativas
220
+ channel.on('presence', { event: 'sync' }, () => {
221
+ const tasks = Object.values(channel.presenceState())
222
+ setTasks(tasks)
223
+ })
224
+ ```
225
+
226
+ **Por quê:** Presence é projetado para "quem está online" — state efêmero ligado a connection. Para listas de objetos, use query normal + broadcast quando muda. Presence inflado degrada toda a infraestrutura Realtime do projeto.
227
+
228
+ **Certo:** query SQL para `tasks` + broadcast em mudanças via trigger DB.
229
+
230
+ ## Ver também
231
+
232
+ - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — RLS sobre `realtime.messages` (SELECT + INSERT separados)
233
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — trigger functions com `set search_path = ''`
234
+ - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que habilita canais `private: true`
235
+ - [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions disparando broadcast via `realtime.send`
236
+ - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: supabase-rls-policies
3
+ description: Use ao criar/auditar RLS — sempre (select auth.uid()), policies separadas por operação, índices nas colunas, NUNCA user_metadata em autorização.
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
+
18
+ ## Regras absolutas
19
+
20
+ **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.
21
+
22
+ **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×**.
23
+
24
+ **Outras regras:**
25
+
26
+ - **`policies separadas por operação`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all` cobrindo CRUD inteiro.
27
+ - **`TO authenticated`** ou **`to anon`** sempre explícito — nunca deixar implícito (default `to public` é insecure).
28
+ - `for select` e `for delete` usam **apenas `using`** (sem `with check`).
29
+ - `for insert` usa **apenas `with check`** (sem `using`).
30
+ - `for update` usa **`using` + `with check`** (using para qual linha pode ser atualizada, with check para qual estado a linha pode assumir).
31
+ - Índice obrigatório nas colunas referenciadas pela policy: `create index on public.tasks (user_id);`. Sem index, scan full em cada query.
32
+ - `permissive` é default e preferido. `restrictive` é raro e exige justificativa explícita.
33
+ - Para MFA enforcement: `(auth.jwt()->>'aal')::text = 'aal2'` em policies que exigem 2FA ativo.
34
+
35
+ ## Patterns canônicos
36
+
37
+ ### SELECT — usuário lê apenas suas próprias linhas
38
+
39
+ ```sql
40
+ -- política de SELECT com wrapper (select auth.uid()) obrigatório
41
+ create policy "users_select_own_tasks"
42
+ on public.tasks
43
+ for select
44
+ to authenticated
45
+ using ((select auth.uid()) = user_id);
46
+
47
+ -- index obrigatório (sem isso, scan full)
48
+ create index tasks_user_id_idx on public.tasks (user_id);
49
+ ```
50
+
51
+ ### INSERT, UPDATE, DELETE separados
52
+
53
+ ```sql
54
+ -- INSERT — usuário só pode criar linhas com user_id = ele mesmo
55
+ create policy "users_insert_own_tasks"
56
+ on public.tasks
57
+ for insert
58
+ to authenticated
59
+ with check ((select auth.uid()) = user_id);
60
+
61
+ -- UPDATE — restringe quais linhas (using) E qual estado novo (with check)
62
+ create policy "users_update_own_tasks"
63
+ on public.tasks
64
+ for update
65
+ to authenticated
66
+ using ((select auth.uid()) = user_id)
67
+ with check ((select auth.uid()) = user_id);
68
+
69
+ -- DELETE — apenas a coluna using (sem with check)
70
+ create policy "users_delete_own_tasks"
71
+ on public.tasks
72
+ for delete
73
+ to authenticated
74
+ using ((select auth.uid()) = user_id);
75
+ ```
76
+
77
+ ### Role admin via `app_metadata`
78
+
79
+ ```sql
80
+ -- segurança: app_metadata é set apenas via service_role (admin API)
81
+ -- cliente NÃO pode mutá-lo
82
+ create policy "admins_manage_all_tasks"
83
+ on public.tasks
84
+ for update
85
+ to authenticated
86
+ using (
87
+ (select auth.jwt()->'app_metadata'->>'role') = 'admin'
88
+ )
89
+ with check (
90
+ (select auth.jwt()->'app_metadata'->>'role') = 'admin'
91
+ );
92
+ ```
93
+
94
+ ### MFA enforcement (AAL2)
95
+
96
+ ```sql
97
+ -- exigir 2FA ativo para acessar dados sensíveis
98
+ create policy "mfa_required_for_billing"
99
+ on public.billing_records
100
+ for select
101
+ to authenticated
102
+ using (
103
+ (select (auth.jwt()->>'aal')::text) = 'aal2'
104
+ and (select auth.uid()) = user_id
105
+ );
106
+ ```
107
+
108
+ ## Anti-patterns
109
+
110
+ ### Anti-pattern 1: `auth.uid()` sem `(select)` wrapper
111
+
112
+ **Errado:**
113
+ ```sql
114
+ create policy "users_select_own_tasks"
115
+ on public.tasks
116
+ for select
117
+ to authenticated
118
+ using (auth.uid() = user_id); -- sem (select) — re-executa por linha
119
+ ```
120
+
121
+ **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).
122
+
123
+ **Certo:**
124
+ ```sql
125
+ using ((select auth.uid()) = user_id)
126
+ ```
127
+
128
+ ### Anti-pattern 2: `WARNING user_metadata` em autorização — privilege escalation
129
+
130
+ **Errado:**
131
+ ```sql
132
+ create policy "admins_manage_all"
133
+ on public.tasks
134
+ for update
135
+ to authenticated
136
+ using (
137
+ (auth.jwt()->'user_metadata'->>'role') = 'admin' -- editável pelo cliente!
138
+ );
139
+ ```
140
+
141
+ **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/).
142
+
143
+ **Certo:** ver "Role admin via `app_metadata`" acima — `app_metadata` requer service_role para mutar.
144
+
145
+ ### Anti-pattern 3: `for all` em vez de policies granulares
146
+
147
+ **Errado:**
148
+ ```sql
149
+ create policy "users_manage_own_tasks"
150
+ on public.tasks
151
+ for all -- cobre CRUD inteiro com mesma regra
152
+ to authenticated
153
+ using ((select auth.uid()) = user_id);
154
+ ```
155
+
156
+ **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.
157
+
158
+ **Certo:** ver pattern com 4 policies separadas acima (SELECT, INSERT, UPDATE, DELETE).
159
+
160
+ ### Anti-pattern 4: Sem índice nas colunas da policy
161
+
162
+ **Errado:**
163
+ ```sql
164
+ -- policy referencia user_id mas não há index
165
+ create policy "users_select_own_tasks" on public.tasks
166
+ for select to authenticated
167
+ using ((select auth.uid()) = user_id);
168
+
169
+ -- (esqueceu) create index on public.tasks (user_id);
170
+ ```
171
+
172
+ **Por quê:** cada query com filtro RLS força sequential scan. Em produção com 100k+ linhas, isso é lentidão crônica.
173
+
174
+ **Certo:**
175
+ ```sql
176
+ create index tasks_user_id_idx on public.tasks (user_id);
177
+ ```
178
+
179
+ ## Ver também
180
+
181
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` que respeitam RLS
182
+ - [supabase-storage](../supabase-storage/SKILL.md) — RLS sobre `storage.objects` (multi-tenant path isolation)
183
+ - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que popula `auth.uid()`
184
+ - [supabase-migrations](../supabase-migrations/SKILL.md) — migrations sempre com RLS habilitado em novas tabelas
185
+ - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + roles + comandos CLI