@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.
- package/CHANGELOG.md +101 -0
- package/gates/agent-no-recursive-dispatch.md +48 -0
- package/gates/budget-description.md +68 -0
- package/gates/no-personal-uuid.md +72 -0
- package/gates/skill-must-include.md +69 -0
- package/gates/sync-idempotent.md +62 -0
- package/kit/agents/codebase-mapper.md +1 -1
- package/kit/agents/executor.md +17 -0
- package/kit/agents/planner.md +35 -0
- package/kit/agents/project-researcher.md +1 -1
- package/kit/agents/schema-checker.md +4 -4
- package/kit/agents/supabase-architect.md +153 -0
- package/kit/agents/supabase-auth-bootstrapper.md +298 -0
- package/kit/agents/supabase-edge-fn-writer.md +185 -0
- package/kit/agents/supabase-migration-writer.md +156 -0
- package/kit/agents/supabase-realtime-implementer.md +252 -0
- package/kit/agents/supabase-rls-writer.md +218 -0
- package/kit/agents/supabase-storage-implementer.md +240 -0
- package/kit/agents/user-profiler.md +1 -1
- package/kit/agents/verifier.md +1 -1
- package/kit/commands/depurar.md +17 -0
- package/kit/commands/fazer.md +15 -0
- package/kit/commands/supabase.md +148 -0
- package/kit/framework/workflows/discuss-phase.md +19 -0
- package/kit/framework/workflows/plan-phase.md +25 -0
- package/kit/skills/_shared-supabase/glossary.md +180 -0
- package/kit/skills/supabase-auth-ssr/SKILL.md +260 -0
- package/kit/skills/supabase-cron-queues/SKILL.md +266 -0
- package/kit/skills/supabase-database-functions/SKILL.md +247 -0
- package/kit/skills/supabase-declarative-schema/SKILL.md +183 -0
- package/kit/skills/supabase-edge-functions/SKILL.md +242 -0
- package/kit/skills/supabase-migrations/SKILL.md +175 -0
- package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -0
- package/kit/skills/supabase-postgres-style/SKILL.md +138 -0
- package/kit/skills/supabase-realtime/SKILL.md +236 -0
- package/kit/skills/supabase-rls-policies/SKILL.md +185 -0
- package/kit/skills/supabase-storage/SKILL.md +234 -0
- 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
|