@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,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-cron-queues
|
|
3
|
+
description: Use ao orquestrar background jobs — pg_cron + pgmq + pg_net pattern cron → pgmq → Edge Function. Sem dep externa. Postgres 15.6.1.143+.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Cron + Queues (background jobs)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando implementar background jobs, scheduled tasks ou queues em Supabase **sem dependência externa** (Inngest, Trigger.dev, etc.). Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "pg_cron", "supabase cron job"
|
|
13
|
+
- "pgmq", "Postgres Message Queue"
|
|
14
|
+
- "pg_net", "HTTP from database"
|
|
15
|
+
- "background job Supabase"
|
|
16
|
+
- "scheduled task Supabase"
|
|
17
|
+
|
|
18
|
+
## Regras absolutas
|
|
19
|
+
|
|
20
|
+
- **Extensions necessárias:**
|
|
21
|
+
- **`pg_cron`** — jobs scheduled (cron syntax)
|
|
22
|
+
- **`pgmq`** — Postgres Message Queue (requer Postgres **15.6.1.143+**)
|
|
23
|
+
- **`pg_net`** — HTTP requests do banco (recomendado v0.10.0+)
|
|
24
|
+
- **Pattern canônico:** **`cron → pgmq → Edge Function`** — `pg_cron` enfileira mensagens em `pgmq`, Edge Function consome (via cron ou polling).
|
|
25
|
+
- **Jobs `pg_cron` curtos** (< 10 min) — jobs longos bloqueiam scheduler. Para jobs longos, enfileire em `pgmq` e processe via Edge Function.
|
|
26
|
+
- **`pgmq.send`** para enfileirar; **`pgmq.read` + `pgmq.archive`** para consumir. Visibility timeout previne double processing.
|
|
27
|
+
- **`pg_net` é async** — `net.http_post` retorna `request_id`, response chega em `net._http_response`. Não bloqueia caller.
|
|
28
|
+
- **Idempotência** — Edge Function consumer deve ser idempotente (mesma mensagem pode ser entregue 2× em retry).
|
|
29
|
+
- **Cleanup** — sem `pgmq.archive` ou `pgmq.delete`, mensagem reaparece após visibility timeout (re-processed).
|
|
30
|
+
|
|
31
|
+
## Patterns canônicos
|
|
32
|
+
|
|
33
|
+
### Setup das extensions + criar fila
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
-- PT-BR: habilitar extensions (uma vez por projeto)
|
|
37
|
+
create extension if not exists pg_cron;
|
|
38
|
+
create extension if not exists pgmq;
|
|
39
|
+
create extension if not exists pg_net;
|
|
40
|
+
|
|
41
|
+
-- PT-BR: criar fila pgmq
|
|
42
|
+
select pgmq.create('email_jobs');
|
|
43
|
+
|
|
44
|
+
-- PT-BR: opcional — criar fila com retention customizado
|
|
45
|
+
-- select pgmq.create_partitioned('large_jobs');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Pattern canônico — `cron → pgmq → Edge Function`
|
|
49
|
+
|
|
50
|
+
```sql
|
|
51
|
+
-- PT-BR: 1. cron job a cada 5 min enfileira pendências em pgmq
|
|
52
|
+
select cron.schedule(
|
|
53
|
+
'enqueue-pending-emails',
|
|
54
|
+
'*/5 * * * *', -- a cada 5 min
|
|
55
|
+
$$
|
|
56
|
+
insert into pgmq.q_email_jobs (message)
|
|
57
|
+
select jsonb_build_object(
|
|
58
|
+
'user_id', id,
|
|
59
|
+
'kind', 'reminder',
|
|
60
|
+
'enqueued_at', now()
|
|
61
|
+
)
|
|
62
|
+
from public.users
|
|
63
|
+
where pending_email = true
|
|
64
|
+
limit 1000; -- batch limitado
|
|
65
|
+
$$
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- PT-BR: 2. cron job a cada minuto despara processamento via Edge Function
|
|
69
|
+
select cron.schedule(
|
|
70
|
+
'process-email-queue',
|
|
71
|
+
'*/1 * * * *', -- a cada minuto
|
|
72
|
+
$$
|
|
73
|
+
select net.http_post(
|
|
74
|
+
url := 'https://<project-ref>.supabase.co/functions/v1/process-emails',
|
|
75
|
+
headers := jsonb_build_object(
|
|
76
|
+
'Content-Type', 'application/json',
|
|
77
|
+
'Authorization', 'Bearer ' || current_setting('supabase.functions_token', true)
|
|
78
|
+
),
|
|
79
|
+
body := '{}'::jsonb
|
|
80
|
+
);
|
|
81
|
+
$$
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Edge Function consumer — pgmq.read + archive
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// supabase/functions/process-emails/index.ts
|
|
89
|
+
// PT-BR: consume da fila pgmq, processa, archive
|
|
90
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
91
|
+
|
|
92
|
+
Deno.serve(async () => {
|
|
93
|
+
const supabase = createClient(
|
|
94
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
95
|
+
Deno.env.get('SUPABASE_SECRET_KEYS')!
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// PT-BR: pegar até 10 mensagens com visibility timeout 30s
|
|
99
|
+
const { data: msgs, error } = await supabase.rpc('pgmq_read', {
|
|
100
|
+
queue_name: 'email_jobs',
|
|
101
|
+
vt: 30, // visibility timeout em segundos
|
|
102
|
+
qty: 10, // máximo por chamada
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (error || !msgs?.length) {
|
|
106
|
+
return new Response('no jobs', { status: 200 })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const m of msgs) {
|
|
110
|
+
try {
|
|
111
|
+
// PT-BR: processar mensagem (idempotente!)
|
|
112
|
+
await sendEmail(m.message.user_id, m.message.kind)
|
|
113
|
+
|
|
114
|
+
// PT-BR: archive remove da fila e move para arquivo
|
|
115
|
+
await supabase.rpc('pgmq_archive', {
|
|
116
|
+
queue_name: 'email_jobs',
|
|
117
|
+
msg_id: m.msg_id,
|
|
118
|
+
})
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// PT-BR: erro — não archive; visibility timeout expira e mensagem reaparece
|
|
121
|
+
console.error('processing error', m.msg_id, err)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Response(`processed ${msgs.length}`)
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Job cron simples — sem queue (cuidado: < 10 min)
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
-- PT-BR: ok para tarefas leves e rápidas (cleanup, agregação)
|
|
133
|
+
select cron.schedule(
|
|
134
|
+
'cleanup-old-sessions',
|
|
135
|
+
'0 3 * * *', -- 3am diário
|
|
136
|
+
$$
|
|
137
|
+
delete from public.sessions where expires_at < now() - interval '30 days';
|
|
138
|
+
$$
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Listar e remover jobs cron
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
-- PT-BR: listar todos os jobs
|
|
146
|
+
select * from cron.job;
|
|
147
|
+
|
|
148
|
+
-- PT-BR: remover job
|
|
149
|
+
select cron.unschedule('process-email-queue');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `pg_net.http_post` async
|
|
153
|
+
|
|
154
|
+
```sql
|
|
155
|
+
-- PT-BR: dispara HTTP request, retorna request_id imediatamente
|
|
156
|
+
select net.http_post(
|
|
157
|
+
url := 'https://api.example.com/webhook',
|
|
158
|
+
headers := jsonb_build_object('Authorization', 'Bearer xxx'),
|
|
159
|
+
body := jsonb_build_object('event', 'task_completed'),
|
|
160
|
+
timeout_milliseconds := 5000
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- PT-BR: response chega em net._http_response (consultar depois)
|
|
164
|
+
select * from net._http_response order by created desc limit 10;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Anti-patterns
|
|
168
|
+
|
|
169
|
+
### Anti-pattern 1: Job cron longo (> 10 min)
|
|
170
|
+
|
|
171
|
+
**Errado:**
|
|
172
|
+
```sql
|
|
173
|
+
select cron.schedule(
|
|
174
|
+
'heavy-batch-process',
|
|
175
|
+
'0 * * * *',
|
|
176
|
+
$$ select pg_sleep(900); ... $$ -- ⚠ 15 min em pg_cron
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Por quê:** `pg_cron` worker bloqueia outros jobs enquanto roda. Se job > 10 min ou trava, scheduler atrasa cascata. Em retry após failure, pode trancar inteiramente.
|
|
181
|
+
|
|
182
|
+
**Certo:** cron enfileira; Edge Function processa pesado:
|
|
183
|
+
```sql
|
|
184
|
+
-- cron: leve (só enfileira)
|
|
185
|
+
select cron.schedule('enqueue-heavy', '0 * * * *', $$
|
|
186
|
+
insert into pgmq.q_heavy_jobs (message) select ...;
|
|
187
|
+
$$);
|
|
188
|
+
-- Edge Function: pesado (consome com timeout próprio)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Anti-pattern 2: HTTP síncrono direto de pg_cron
|
|
192
|
+
|
|
193
|
+
**Errado:**
|
|
194
|
+
```sql
|
|
195
|
+
select cron.schedule('call-api', '*/1 * * * *', $$
|
|
196
|
+
-- ⚠ pg_net é async, mas user pode tentar sync com loops
|
|
197
|
+
select net.http_get('https://api.example.com/long');
|
|
198
|
+
$$);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Por quê:** HTTP requests podem demorar segundos a minutos. Se response demora, próxima execução do cron empilha. Em alta latência, scheduler fica trancado.
|
|
202
|
+
|
|
203
|
+
**Certo:** enfileire em pgmq + Edge Function processa:
|
|
204
|
+
```sql
|
|
205
|
+
-- cron: enfileira
|
|
206
|
+
insert into pgmq.q_api_calls (message) values ('{"endpoint": "/long"}');
|
|
207
|
+
-- Edge Function: chama API com timeout próprio + archive
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Anti-pattern 3: `pgmq.read` sem `archive` ou `delete`
|
|
211
|
+
|
|
212
|
+
**Errado:**
|
|
213
|
+
```ts
|
|
214
|
+
const { data: msgs } = await supabase.rpc('pgmq_read', { queue_name: 'jobs', vt: 30, qty: 10 })
|
|
215
|
+
for (const m of msgs) {
|
|
216
|
+
await processJob(m.message)
|
|
217
|
+
// ⚠ esqueceu pgmq_archive
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Por quê:** após visibility timeout (30s), mensagem reaparece — mesmo job rodado novamente. Em loop, leva a re-processing infinito.
|
|
222
|
+
|
|
223
|
+
**Certo:**
|
|
224
|
+
```ts
|
|
225
|
+
for (const m of msgs) {
|
|
226
|
+
try {
|
|
227
|
+
await processJob(m.message)
|
|
228
|
+
await supabase.rpc('pgmq_archive', { queue_name: 'jobs', msg_id: m.msg_id })
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// PT-BR: NÃO archive; mensagem retorna após vt para retry
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Anti-pattern 4: Edge Function não-idempotente
|
|
236
|
+
|
|
237
|
+
**Errado:**
|
|
238
|
+
```ts
|
|
239
|
+
async function processJob(msg) {
|
|
240
|
+
await sendEmail(msg.user_id) // ⚠ envia email mesmo se já enviado
|
|
241
|
+
await chargeCard(msg.amount) // ⚠ cobra mesmo se já cobrado
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Por quê:** retries entregam mesma mensagem 2×+. Sem idempotência, side effects duplicam — usuário recebe 2 emails ou é cobrado 2×.
|
|
246
|
+
|
|
247
|
+
**Certo:** rastreie estado:
|
|
248
|
+
```ts
|
|
249
|
+
async function processJob(msg) {
|
|
250
|
+
const { data: existing } = await supabase
|
|
251
|
+
.from('email_log')
|
|
252
|
+
.select('id')
|
|
253
|
+
.eq('msg_id', msg.id)
|
|
254
|
+
.single()
|
|
255
|
+
if (existing) return // já processado
|
|
256
|
+
await sendEmail(msg.user_id)
|
|
257
|
+
await supabase.from('email_log').insert({ msg_id: msg.id })
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Ver também
|
|
262
|
+
|
|
263
|
+
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions consumindo pgmq
|
|
264
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` chamadas em cron
|
|
265
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — extensions criadas em migrations
|
|
266
|
+
- [glossário](../_shared-supabase/glossary.md) — pg_cron, pgmq, pg_net
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-database-functions
|
|
3
|
+
description: Use ao criar funções Postgres — SECURITY INVOKER por padrão, SET search_path = '' SEMPRE, schema-qualified names, IMMUTABLE/STABLE quando possível.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Database Functions
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando criar ou auditar funções Postgres em projeto Supabase. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "criar função Postgres", "create or replace function"
|
|
13
|
+
- "trigger de banco", "function trigger"
|
|
14
|
+
- "SECURITY INVOKER vs DEFINER"
|
|
15
|
+
- "search_path", "set search_path"
|
|
16
|
+
- "função imutável", "stable function"
|
|
17
|
+
|
|
18
|
+
## Regras absolutas
|
|
19
|
+
|
|
20
|
+
- **Sempre `SECURITY INVOKER`** por default — função roda com permissões de quem invoca (mais seguro). `SECURITY DEFINER` apenas com justificativa explícita escrita em comentário no topo da função.
|
|
21
|
+
- **Sempre `set search_path = ''`** — sem isso, função vulnerável a hijack de schema. Documentado em [Database Advisors lint 0011](https://supabase.com/docs/guides/database/database-advisors).
|
|
22
|
+
- **Schema-qualified** (em todas as referências a tabelas, colunas, outras funções): `public.tasks`, não `tasks`. Sem qualifier, lookup falha quando `search_path = ''`.
|
|
23
|
+
- Marque **`IMMUTABLE`** se função não consulta DB e sempre retorna o mesmo para os mesmos inputs (ex: formatadores de string).
|
|
24
|
+
- Marque **`STABLE`** se função consulta DB mas não modifica e retorna o mesmo dentro de uma transação (ex: lookups). Permite Postgres cachear o resultado por query.
|
|
25
|
+
- Use **`VOLATILE`** apenas se função modifica dados ou tem side effects (default — não precisa explicitar).
|
|
26
|
+
- Error handling com `RAISE EXCEPTION 'mensagem'` — nunca silent fail.
|
|
27
|
+
- Para triggers: include `CREATE TRIGGER` válido junto com `CREATE FUNCTION` na mesma migration.
|
|
28
|
+
|
|
29
|
+
## Patterns canônicos
|
|
30
|
+
|
|
31
|
+
### Função simples — SECURITY INVOKER + search_path
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
-- formatador puro: IMMUTABLE
|
|
35
|
+
create or replace function public.format_full_name(first_name text, last_name text)
|
|
36
|
+
returns text
|
|
37
|
+
language sql
|
|
38
|
+
security invoker
|
|
39
|
+
set search_path = ''
|
|
40
|
+
immutable
|
|
41
|
+
as $$
|
|
42
|
+
select first_name || ' ' || last_name;
|
|
43
|
+
$$;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Função com query — STABLE + schema-qualified
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
-- conta tasks de um usuário (não modifica) — STABLE permite caching
|
|
50
|
+
create or replace function public.get_user_task_count(p_user_id uuid)
|
|
51
|
+
returns integer
|
|
52
|
+
language plpgsql
|
|
53
|
+
security invoker
|
|
54
|
+
set search_path = ''
|
|
55
|
+
stable
|
|
56
|
+
as $$
|
|
57
|
+
declare
|
|
58
|
+
v_count integer;
|
|
59
|
+
begin
|
|
60
|
+
select count(*) into v_count
|
|
61
|
+
from public.tasks -- schema-qualified obrigatório
|
|
62
|
+
where user_id = p_user_id;
|
|
63
|
+
return v_count;
|
|
64
|
+
end;
|
|
65
|
+
$$;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Trigger — atualizar `updated_at`
|
|
69
|
+
|
|
70
|
+
```sql
|
|
71
|
+
-- function + trigger juntos na mesma migration
|
|
72
|
+
create or replace function public.set_updated_at()
|
|
73
|
+
returns trigger
|
|
74
|
+
language plpgsql
|
|
75
|
+
security invoker
|
|
76
|
+
set search_path = ''
|
|
77
|
+
as $$
|
|
78
|
+
begin
|
|
79
|
+
new.updated_at := now();
|
|
80
|
+
return new;
|
|
81
|
+
end;
|
|
82
|
+
$$;
|
|
83
|
+
|
|
84
|
+
create trigger tasks_set_updated_at
|
|
85
|
+
before update on public.tasks
|
|
86
|
+
for each row
|
|
87
|
+
execute function public.set_updated_at();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Função com error handling
|
|
91
|
+
|
|
92
|
+
```sql
|
|
93
|
+
create or replace function public.transfer_credits(
|
|
94
|
+
p_from_user uuid,
|
|
95
|
+
p_to_user uuid,
|
|
96
|
+
p_amount integer
|
|
97
|
+
)
|
|
98
|
+
returns void
|
|
99
|
+
language plpgsql
|
|
100
|
+
security invoker
|
|
101
|
+
set search_path = ''
|
|
102
|
+
as $$
|
|
103
|
+
declare
|
|
104
|
+
v_from_balance integer;
|
|
105
|
+
begin
|
|
106
|
+
if p_amount <= 0 then
|
|
107
|
+
raise exception 'Valor de transferência deve ser positivo: %', p_amount;
|
|
108
|
+
end if;
|
|
109
|
+
|
|
110
|
+
select balance into v_from_balance
|
|
111
|
+
from public.accounts
|
|
112
|
+
where user_id = p_from_user
|
|
113
|
+
for update; -- lock para evitar race
|
|
114
|
+
|
|
115
|
+
if v_from_balance < p_amount then
|
|
116
|
+
raise exception 'Saldo insuficiente';
|
|
117
|
+
end if;
|
|
118
|
+
|
|
119
|
+
update public.accounts
|
|
120
|
+
set balance = balance - p_amount
|
|
121
|
+
where user_id = p_from_user;
|
|
122
|
+
|
|
123
|
+
update public.accounts
|
|
124
|
+
set balance = balance + p_amount
|
|
125
|
+
where user_id = p_to_user;
|
|
126
|
+
end;
|
|
127
|
+
$$;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `SECURITY DEFINER` — quando justificável
|
|
131
|
+
|
|
132
|
+
```sql
|
|
133
|
+
-- caso raro: função precisa fazer algo que invoker não pode fazer
|
|
134
|
+
-- ex: contar todos os usuários (acessível só para admins via app_metadata)
|
|
135
|
+
-- mas exposto via RPC para qualquer authenticated com auth check interno
|
|
136
|
+
|
|
137
|
+
-- comentário JUSTIFICANDO o DEFINER (obrigatório)
|
|
138
|
+
create or replace function public.count_active_users()
|
|
139
|
+
returns integer
|
|
140
|
+
-- security definer porque: precisamos bypassar RLS de auth.users que bloqueia leitura
|
|
141
|
+
-- mitigação: validamos role admin via app_metadata logo no topo
|
|
142
|
+
language plpgsql
|
|
143
|
+
security definer
|
|
144
|
+
set search_path = ''
|
|
145
|
+
stable
|
|
146
|
+
as $$
|
|
147
|
+
declare
|
|
148
|
+
v_count integer;
|
|
149
|
+
begin
|
|
150
|
+
-- validar admin via app_metadata (não user_metadata!)
|
|
151
|
+
if (auth.jwt()->'app_metadata'->>'role') is distinct from 'admin' then
|
|
152
|
+
raise exception 'Acesso negado: apenas admins';
|
|
153
|
+
end if;
|
|
154
|
+
|
|
155
|
+
select count(*) into v_count
|
|
156
|
+
from public.users
|
|
157
|
+
where last_seen_at > now() - interval '30 days';
|
|
158
|
+
return v_count;
|
|
159
|
+
end;
|
|
160
|
+
$$;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Anti-patterns
|
|
164
|
+
|
|
165
|
+
### Anti-pattern 1: `SECURITY DEFINER` + sem `set search_path` + sem schema qualifier
|
|
166
|
+
|
|
167
|
+
**Errado:**
|
|
168
|
+
```sql
|
|
169
|
+
create or replace function f()
|
|
170
|
+
returns integer
|
|
171
|
+
language plpgsql
|
|
172
|
+
security definer -- ⚠ sem justificativa
|
|
173
|
+
as $$ -- ⚠ sem set search_path
|
|
174
|
+
begin
|
|
175
|
+
return (select count(*) from tasks); -- ⚠ sem public. qualifier
|
|
176
|
+
end;
|
|
177
|
+
$$;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Por quê:** atacante pode criar `tasks` em schema próprio + manipular `search_path` via `set local search_path = atacante,public` antes de invocar. Função `SECURITY DEFINER` executa com permissões do owner — atacante consegue ler/escrever onde não deveria.
|
|
181
|
+
|
|
182
|
+
**Certo:**
|
|
183
|
+
```sql
|
|
184
|
+
create or replace function public.f()
|
|
185
|
+
returns integer
|
|
186
|
+
language plpgsql
|
|
187
|
+
security invoker -- prefira invoker
|
|
188
|
+
set search_path = '' -- bloqueia hijack
|
|
189
|
+
stable
|
|
190
|
+
as $$
|
|
191
|
+
begin
|
|
192
|
+
return (select count(*) from public.tasks); -- qualified
|
|
193
|
+
end;
|
|
194
|
+
$$;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Anti-pattern 2: Função consulta DB mas marcada `IMMUTABLE`
|
|
198
|
+
|
|
199
|
+
**Errado:**
|
|
200
|
+
```sql
|
|
201
|
+
create or replace function public.user_count_immutable()
|
|
202
|
+
returns integer
|
|
203
|
+
language sql
|
|
204
|
+
immutable -- ⚠ função consulta DB — não imutável
|
|
205
|
+
set search_path = ''
|
|
206
|
+
as $$
|
|
207
|
+
select count(*) from public.users;
|
|
208
|
+
$$;
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Por quê:** `IMMUTABLE` diz para Postgres "este resultado nunca muda para os mesmos inputs". Postgres pode cachear ou pré-computar. Mas a contagem de usuários muda — Postgres pode retornar valor stale indefinidamente.
|
|
212
|
+
|
|
213
|
+
**Certo:** usar `stable` (consulta DB, não modifica, mesmo em uma transação) ou `volatile` (default — recompute sempre).
|
|
214
|
+
|
|
215
|
+
### Anti-pattern 3: Silent fail sem `raise exception`
|
|
216
|
+
|
|
217
|
+
**Errado:**
|
|
218
|
+
```sql
|
|
219
|
+
create or replace function public.deduct_credits(p_user uuid, p_amount integer)
|
|
220
|
+
returns void
|
|
221
|
+
language plpgsql
|
|
222
|
+
security invoker
|
|
223
|
+
set search_path = ''
|
|
224
|
+
as $$
|
|
225
|
+
begin
|
|
226
|
+
-- ⚠ sem validação — atualiza mesmo com saldo negativo
|
|
227
|
+
update public.accounts
|
|
228
|
+
set balance = balance - p_amount
|
|
229
|
+
where user_id = p_user;
|
|
230
|
+
end;
|
|
231
|
+
$$;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Por quê:** silent fail oculta bugs. Saldo fica negativo sem aviso; testes downstream falham com mensagens enigmáticas.
|
|
235
|
+
|
|
236
|
+
**Certo:**
|
|
237
|
+
```sql
|
|
238
|
+
-- valida + raise exception se inválido (ver pattern "transfer_credits" acima)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Ver também
|
|
242
|
+
|
|
243
|
+
- [supabase-postgres-style](../supabase-postgres-style/SKILL.md) — convenção de naming + style aplicada em funções
|
|
244
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — funções e RLS interagem (SECURITY INVOKER respeita RLS do invoker)
|
|
245
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — funções em migrations são versionadas
|
|
246
|
+
- [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — funções invocadas por `pg_cron` jobs
|
|
247
|
+
- [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + comandos CLI
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-declarative-schema
|
|
3
|
+
description: Use ao gerenciar schema via supabase/schemas/ — workflow stop → db diff -f → revisar → apply. Inclui caveats sobre views, RLS, partitions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Declarative Database Schema
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando trabalhar com `supabase/schemas/` (declarative source-of-truth) em vez de migrations imperativas. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "supabase schemas/", "declarative schema"
|
|
13
|
+
- "supabase db diff", "gerar migration de schema"
|
|
14
|
+
- "schema source of truth"
|
|
15
|
+
- "como adicionar tabela em projeto declarative"
|
|
16
|
+
|
|
17
|
+
## Regras absolutas
|
|
18
|
+
|
|
19
|
+
- **Workflow canônico:**
|
|
20
|
+
1. Editar arquivos `.sql` em `supabase/schemas/` (representando estado **final** desejado de cada entidade)
|
|
21
|
+
2. **`supabase stop`** — derrubar containers locais (necessário antes de diff)
|
|
22
|
+
3. **`supabase db diff -f <name>`** — gera migration em `supabase/migrations/<timestamp>_<name>.sql`
|
|
23
|
+
4. **Revisar manualmente** a migration gerada (diff é heurístico — pode gerar SQL incorreto em renames, drops, etc.)
|
|
24
|
+
5. `supabase db reset` para aplicar local; `supabase db push` para aplicar remote
|
|
25
|
+
- **Nunca pule `supabase stop`** antes de `db diff` — diff sem stop produz output inconsistente.
|
|
26
|
+
- **Nunca pule revisão** da migration gerada — especialmente para renames (diff pode gerar `drop+create` em vez de `rename column`).
|
|
27
|
+
- **DML (INSERT/UPDATE/DELETE) NÃO é declarative** — fica em migrations imperativas (`supabase/migrations/`) ou `supabase/seed.sql`.
|
|
28
|
+
- **Files ordenados lexicograficamente** — para gerenciar dependências (FKs), nomeie de forma que a ordem de execução resolva referências (ex: `01_users.sql`, `02_tasks.sql`).
|
|
29
|
+
- **Adicione novas colunas no fim** da definição da tabela — evita diffs falsos em PRs.
|
|
30
|
+
- Seu `kit` de schemas reflete estado final, **não** o histórico — migrations carregam o histórico.
|
|
31
|
+
|
|
32
|
+
## Patterns canônicos
|
|
33
|
+
|
|
34
|
+
### Estrutura típica de `supabase/schemas/`
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
supabase/
|
|
38
|
+
├── schemas/
|
|
39
|
+
│ ├── 01_extensions.sql -- create extension if not exists ...
|
|
40
|
+
│ ├── 02_users.sql -- public.users (mirror de auth.users)
|
|
41
|
+
│ ├── 03_tasks.sql -- public.tasks
|
|
42
|
+
│ ├── 04_tasks_rls.sql -- policies em public.tasks
|
|
43
|
+
│ └── 05_functions.sql -- public.set_updated_at, etc.
|
|
44
|
+
├── migrations/ -- gerado por db diff (revisado e commitado)
|
|
45
|
+
└── seed.sql -- DML (não declarative)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Workflow de mudança
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# PT-BR: 1. editar schemas/
|
|
52
|
+
# (ex: adicionar coluna priority em supabase/schemas/03_tasks.sql)
|
|
53
|
+
|
|
54
|
+
# PT-BR: 2. parar containers (obrigatório antes de diff)
|
|
55
|
+
supabase stop
|
|
56
|
+
|
|
57
|
+
# PT-BR: 3. gerar migration
|
|
58
|
+
supabase db diff -f add_priority_to_tasks
|
|
59
|
+
|
|
60
|
+
# PT-BR: 4. revisar arquivo gerado
|
|
61
|
+
# supabase/migrations/<timestamp>_add_priority_to_tasks.sql
|
|
62
|
+
# (verificar se diff capturou só o intended change — não renames falsos, drops indevidos)
|
|
63
|
+
|
|
64
|
+
# PT-BR: 5. aplicar local
|
|
65
|
+
supabase db reset
|
|
66
|
+
|
|
67
|
+
# PT-BR: 6. (depois) aplicar remote
|
|
68
|
+
supabase db push
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Schema com FK e RLS
|
|
72
|
+
|
|
73
|
+
```sql
|
|
74
|
+
-- supabase/schemas/03_tasks.sql
|
|
75
|
+
create table if not exists public.tasks (
|
|
76
|
+
id uuid primary key default gen_random_uuid(),
|
|
77
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
78
|
+
title text not null,
|
|
79
|
+
status text not null default 'todo',
|
|
80
|
+
priority text not null default 'low', -- novas colunas: append no fim
|
|
81
|
+
created_at timestamptz not null default now()
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
alter table public.tasks enable row level security;
|
|
85
|
+
|
|
86
|
+
-- policies em arquivo separado (04_tasks_rls.sql) ou aqui
|
|
87
|
+
-- mas sempre granulares (ver supabase-rls-policies)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Caveats — limitações conhecidas do declarative
|
|
91
|
+
|
|
92
|
+
O `migra` diff tool (usado por `supabase db diff`) tem edge cases. **Sempre revise** a migration gerada antes de aplicar.
|
|
93
|
+
|
|
94
|
+
### DML (INSERT/UPDATE/DELETE)
|
|
95
|
+
- **Não rastreável** por declarative (declarative só captura DDL — Data Definition Language).
|
|
96
|
+
- Use `supabase/seed.sql` para seed data ou migrations imperativas para mudanças de dados.
|
|
97
|
+
|
|
98
|
+
### View ownership e atributos
|
|
99
|
+
- Diff **não captura mudanças de owner** de views.
|
|
100
|
+
- **Security invoker em views** não é diferenciado por diff — usar migration manual se mudar.
|
|
101
|
+
- **Materialized views** têm suporte limitado.
|
|
102
|
+
- **Mudança de column type em views** não recria a view — diff pode falhar silenciosamente.
|
|
103
|
+
|
|
104
|
+
### RLS policies
|
|
105
|
+
- `alter policy` statements são suportados mas podem ter edge cases.
|
|
106
|
+
- **Column privileges** não são totalmente capturados.
|
|
107
|
+
|
|
108
|
+
### Outras entidades
|
|
109
|
+
- **Schema privileges:** não rastreados (cada schema diffado separadamente).
|
|
110
|
+
- **Comments on objects:** não rastreados.
|
|
111
|
+
- **Partitions:** suporte limitado — partitioned tables podem precisar migration manual.
|
|
112
|
+
- **`alter publication ... add table`:** não detectado por diff.
|
|
113
|
+
- **`create domain`:** ignorado por diff (usar migration imperativa).
|
|
114
|
+
- **`grant` statements:** duplicados a partir de default privileges — verificar saída.
|
|
115
|
+
|
|
116
|
+
## Anti-patterns
|
|
117
|
+
|
|
118
|
+
### Anti-pattern 1: `db diff` com containers up
|
|
119
|
+
|
|
120
|
+
**Errado:**
|
|
121
|
+
```bash
|
|
122
|
+
# containers ainda rodando
|
|
123
|
+
supabase db diff -f my_change # ⚠ output inconsistente
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Por quê:** diff compara schema declarado em `schemas/` com DB local atual. Se containers up, DB tem state inconsistente (mid-transaction, locks abertos).
|
|
127
|
+
|
|
128
|
+
**Certo:**
|
|
129
|
+
```bash
|
|
130
|
+
supabase stop
|
|
131
|
+
supabase db diff -f my_change
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Anti-pattern 2: Aplicar migration gerada sem revisão
|
|
135
|
+
|
|
136
|
+
**Errado:**
|
|
137
|
+
```bash
|
|
138
|
+
supabase db diff -f rename_column
|
|
139
|
+
supabase db push # ⚠ aplicou sem revisar
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Por quê:** diff é heurístico. Em renames, pode gerar `drop column old + create column new` em vez de `alter table ... rename column`. Resultado: dados perdidos.
|
|
143
|
+
|
|
144
|
+
**Certo:** sempre abrir `supabase/migrations/<timestamp>_*.sql` e revisar antes de aplicar.
|
|
145
|
+
|
|
146
|
+
### Anti-pattern 3: DML em `supabase/schemas/`
|
|
147
|
+
|
|
148
|
+
**Errado:**
|
|
149
|
+
```sql
|
|
150
|
+
-- supabase/schemas/03_tasks.sql
|
|
151
|
+
create table if not exists public.tasks (...);
|
|
152
|
+
|
|
153
|
+
insert into public.tasks (id, title) values (...); -- ⚠ DML não é declarative
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Por quê:** declarative captura apenas DDL. Inserts em `schemas/` rodam quando schema é aplicado, mas não são rastreáveis em migrations — recriam sempre que `db reset`.
|
|
157
|
+
|
|
158
|
+
**Certo:** mover INSERTs para `supabase/seed.sql` ou migration imperativa.
|
|
159
|
+
|
|
160
|
+
### Anti-pattern 4: Adicionar coluna no meio da definição
|
|
161
|
+
|
|
162
|
+
**Errado:**
|
|
163
|
+
```sql
|
|
164
|
+
-- antes
|
|
165
|
+
create table public.tasks (id uuid, title text, created_at timestamptz);
|
|
166
|
+
|
|
167
|
+
-- depois (coluna adicionada NO MEIO)
|
|
168
|
+
create table public.tasks (id uuid, priority text, title text, created_at timestamptz);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Por quê:** diff pode interpretar como reorder e gerar SQL ineficiente (drop + recreate de várias colunas).
|
|
172
|
+
|
|
173
|
+
**Certo:** appendar no fim:
|
|
174
|
+
```sql
|
|
175
|
+
create table public.tasks (id uuid, title text, created_at timestamptz, priority text);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Ver também
|
|
179
|
+
|
|
180
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — formato e regras dos arquivos gerados em `migrations/`
|
|
181
|
+
- [supabase-postgres-style](../supabase-postgres-style/SKILL.md) — estilo SQL nas declarações
|
|
182
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — como expressar RLS em schemas/
|
|
183
|
+
- [glossário](../_shared-supabase/glossary.md) — comandos CLI canônicos (`supabase stop`, `db diff -f`, `db reset`)
|