@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,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-edge-functions
|
|
3
|
+
description: Use ao escrever Edge Functions — Deno + imports npm:/jsr: (NUNCA bare), Deno.serve, env vars pre-populadas, file writes APENAS em /tmp, EdgeRuntime.waitUntil.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Edge Functions (Deno)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando criar, editar ou debugar Supabase Edge Functions (Deno runtime). Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "criar Edge Function", "Supabase functions"
|
|
13
|
+
- "Deno + Supabase"
|
|
14
|
+
- "supabase functions deploy"
|
|
15
|
+
- "Edge Function background task"
|
|
16
|
+
- "import npm: jsr: em Edge Function"
|
|
17
|
+
|
|
18
|
+
## Regras absolutas
|
|
19
|
+
|
|
20
|
+
- **Runtime é Deno**, não Node.js. Use APIs Deno (`Deno.serve`, `Deno.env`, `Deno.writeTextFile`).
|
|
21
|
+
- **Imports SEMPRE com `npm:` ou `jsr:`** prefix. **NUNCA** bare specifiers (`import x from 'pkg'` falha em runtime).
|
|
22
|
+
- **Use versão pinada** nos imports — `npm:hono@4.6.7`, `npm:@supabase/supabase-js@2`. Sem version, runtime resolve para latest e quebra em deploy.
|
|
23
|
+
- **Env vars pre-populadas** (não definir manualmente):
|
|
24
|
+
- `SUPABASE_URL`
|
|
25
|
+
- `SUPABASE_PUBLISHABLE_KEYS` (anon key — para client-side context)
|
|
26
|
+
- `SUPABASE_SECRET_KEYS` (service role — server-side only)
|
|
27
|
+
- `SUPABASE_DB_URL` (conexão direta ao Postgres)
|
|
28
|
+
- Para outros secrets, set via `supabase secrets set --env-file path/to/.env`.
|
|
29
|
+
- **`Deno.serve`** é o entry point canônico. **Nunca** `addEventListener('fetch')` (deprecated) ou `serve` de `https://deno.land/std@0.168.0/http/server.ts` (não usar).
|
|
30
|
+
- **File writes APENAS em `/tmp`** — qualquer outro path é read-only.
|
|
31
|
+
- Para tarefas em background após resposta, use **`EdgeRuntime.waitUntil(promise)`**. Sem isso, função termina antes da promise.
|
|
32
|
+
- Multi-rota com Hono ou Express deve **prefixar** todas as rotas com `/<function-name>` (ex: `/my-function/users`) — sem prefix, request 404 quando deployada.
|
|
33
|
+
|
|
34
|
+
## Patterns canônicos
|
|
35
|
+
|
|
36
|
+
### Função básica — Deno.serve + npm: import
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// supabase/functions/hello/index.ts
|
|
40
|
+
// PT-BR: imports versionados sempre com npm:
|
|
41
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
42
|
+
|
|
43
|
+
Deno.serve(async (req) => {
|
|
44
|
+
const supabase = createClient(
|
|
45
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
46
|
+
Deno.env.get('SUPABASE_SECRET_KEYS')! // service role server-side
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const { data, error } = await supabase
|
|
50
|
+
.from('tasks')
|
|
51
|
+
.select('id, title')
|
|
52
|
+
.limit(10)
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
56
|
+
status: 500,
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Response(JSON.stringify(data), {
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Background task com `EdgeRuntime.waitUntil`
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// supabase/functions/audit-log/index.ts
|
|
71
|
+
// PT-BR: responde rápido, processa pesado em background
|
|
72
|
+
|
|
73
|
+
Deno.serve(async (req) => {
|
|
74
|
+
const body = await req.json()
|
|
75
|
+
|
|
76
|
+
// PT-BR: `waitUntil` mantém runtime alive até promise resolver
|
|
77
|
+
EdgeRuntime.waitUntil((async () => {
|
|
78
|
+
// PT-BR: file write apenas em /tmp
|
|
79
|
+
await Deno.writeTextFile(
|
|
80
|
+
`/tmp/audit-${Date.now()}.log`,
|
|
81
|
+
JSON.stringify(body)
|
|
82
|
+
)
|
|
83
|
+
// PT-BR: pode chamar APIs externas, gerar embeddings, etc.
|
|
84
|
+
await fetch('https://example.com/audit', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
})
|
|
88
|
+
})())
|
|
89
|
+
|
|
90
|
+
// PT-BR: response volta imediatamente
|
|
91
|
+
return new Response('accepted', { status: 202 })
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Multi-rota com Hono
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// supabase/functions/api/index.ts
|
|
99
|
+
// PT-BR: rotas prefixadas com /api (nome da function)
|
|
100
|
+
import { Hono } from 'npm:hono@4.6.7'
|
|
101
|
+
|
|
102
|
+
const app = new Hono().basePath('/api')
|
|
103
|
+
|
|
104
|
+
app.get('/users', (c) => c.json({ users: [] }))
|
|
105
|
+
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
|
|
106
|
+
app.post('/users', async (c) => {
|
|
107
|
+
const body = await c.req.json()
|
|
108
|
+
return c.json({ created: body }, 201)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
Deno.serve(app.fetch)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Função usando JSR e Node built-in
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// supabase/functions/hash/index.ts
|
|
118
|
+
// PT-BR: imports do JSR + Node built-in (precisa node: prefix)
|
|
119
|
+
import { encodeHex } from 'jsr:@std/encoding/hex'
|
|
120
|
+
import { createHash } from 'node:crypto'
|
|
121
|
+
|
|
122
|
+
Deno.serve(async (req) => {
|
|
123
|
+
const { text } = await req.json()
|
|
124
|
+
const hash = createHash('sha256').update(text).digest('hex')
|
|
125
|
+
return new Response(JSON.stringify({ hash }), {
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Auth — service-role server-side
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// supabase/functions/admin-action/index.ts
|
|
135
|
+
// PT-BR: service-role bypassa RLS — apenas server-side
|
|
136
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
137
|
+
|
|
138
|
+
Deno.serve(async (req) => {
|
|
139
|
+
// PT-BR: extrair JWT do header Authorization e validar
|
|
140
|
+
const authHeader = req.headers.get('Authorization')
|
|
141
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
142
|
+
return new Response('unauthorized', { status: 401 })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// PT-BR: client com service-role para operação privilegiada
|
|
146
|
+
const supabase = createClient(
|
|
147
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
148
|
+
Deno.env.get('SUPABASE_SECRET_KEYS')!
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// PT-BR: validar JWT e extrair user
|
|
152
|
+
const { data: { user }, error } = await supabase.auth.getUser(
|
|
153
|
+
authHeader.replace('Bearer ', '')
|
|
154
|
+
)
|
|
155
|
+
if (!user || error) return new Response('unauthorized', { status: 401 })
|
|
156
|
+
|
|
157
|
+
// PT-BR: agora pode operar com privilégios de service_role
|
|
158
|
+
await supabase.from('audit_log').insert({ user_id: user.id, action: 'admin_view' })
|
|
159
|
+
|
|
160
|
+
return new Response('ok')
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Anti-patterns
|
|
165
|
+
|
|
166
|
+
### Anti-pattern 1: Bare specifier sem `npm:`/`jsr:`
|
|
167
|
+
|
|
168
|
+
**Errado:**
|
|
169
|
+
```ts
|
|
170
|
+
import { createClient } from '@supabase/supabase-js' // ⚠ bare specifier
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Por quê:** Deno não resolve bare specifiers. Runtime falha em startup com erro `Module not found`.
|
|
174
|
+
|
|
175
|
+
**Certo:**
|
|
176
|
+
```ts
|
|
177
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Anti-pattern 2: `Deno.writeTextFile` fora de `/tmp`
|
|
181
|
+
|
|
182
|
+
**Errado:**
|
|
183
|
+
```ts
|
|
184
|
+
await Deno.writeTextFile('/data/audit.log', data) // ⚠ filesystem read-only
|
|
185
|
+
await Deno.writeTextFile('./local/x.log', data) // ⚠ idem
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Por quê:** Edge Functions runtime tem filesystem read-only exceto `/tmp`. Writes fora de `/tmp` falham com `EACCES`.
|
|
189
|
+
|
|
190
|
+
**Certo:**
|
|
191
|
+
```ts
|
|
192
|
+
await Deno.writeTextFile(`/tmp/audit-${Date.now()}.log`, data)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Anti-pattern 3: Trabalho pesado inline na response
|
|
196
|
+
|
|
197
|
+
**Errado:**
|
|
198
|
+
```ts
|
|
199
|
+
Deno.serve(async (req) => {
|
|
200
|
+
const body = await req.json()
|
|
201
|
+
await processHeavyJob(body) // ⚠ trava response 30s+
|
|
202
|
+
await sendEmail(body) // ⚠ idem
|
|
203
|
+
return new Response('done')
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Por quê:** cliente espera resposta. Edge Functions têm timeout (default 60s). Falhas pontuais quebram UX.
|
|
208
|
+
|
|
209
|
+
**Certo:** use `EdgeRuntime.waitUntil` para liberar resposta:
|
|
210
|
+
```ts
|
|
211
|
+
Deno.serve(async (req) => {
|
|
212
|
+
const body = await req.json()
|
|
213
|
+
EdgeRuntime.waitUntil((async () => {
|
|
214
|
+
await processHeavyJob(body)
|
|
215
|
+
await sendEmail(body)
|
|
216
|
+
})())
|
|
217
|
+
return new Response('accepted', { status: 202 })
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Anti-pattern 4: Multi-rota sem prefix
|
|
222
|
+
|
|
223
|
+
**Errado:**
|
|
224
|
+
```ts
|
|
225
|
+
const app = new Hono() // ⚠ sem basePath
|
|
226
|
+
app.get('/users', handler)
|
|
227
|
+
Deno.serve(app.fetch) // request a /users → 404 em produção
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Por quê:** quando deployado, URL é `https://<ref>.supabase.co/functions/v1/<name>/...`. Sem `basePath('/<name>')` no router, request a `/users` não casa.
|
|
231
|
+
|
|
232
|
+
**Certo:**
|
|
233
|
+
```ts
|
|
234
|
+
const app = new Hono().basePath('/api') // PT-BR: prefix com nome da function
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Ver também
|
|
238
|
+
|
|
239
|
+
- [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — clients usam `npm:@supabase/supabase-js`
|
|
240
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — service-role server-side bypassa RLS
|
|
241
|
+
- [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — Edge Functions invocadas por `pg_net.http_post`
|
|
242
|
+
- [glossário](../_shared-supabase/glossary.md) — comandos CLI (`supabase functions deploy`)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-migrations
|
|
3
|
+
description: Use ao criar arquivos de migration Supabase — naming YYYYMMDDHHmmss_short.sql, header de metadados, RLS obrigatório em toda nova tabela, granular policies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Migrations
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando criar/editar arquivos em `supabase/migrations/`. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "criar migration Supabase", "supabase migration new"
|
|
13
|
+
- "alterar schema do banco", "alter table"
|
|
14
|
+
- "criar nova tabela em Postgres/Supabase"
|
|
15
|
+
- "adicionar coluna a tabela existente"
|
|
16
|
+
- "drop column / drop table" (operações destrutivas — exige cuidado extra)
|
|
17
|
+
|
|
18
|
+
## Regras absolutas
|
|
19
|
+
|
|
20
|
+
- **Naming canônico:** `YYYYMMDDHHmmss_short_description.sql` em UTC (ex: `20260506120000_create_tasks.sql`). Use `supabase migration new <name>` para gerar timestamp correto.
|
|
21
|
+
- **Header de metadados** no topo de cada migration (block comment) descrevendo Migration / Created / Purpose / Affects.
|
|
22
|
+
- **lowercase em todo SQL** (alinhado com `supabase-postgres-style`).
|
|
23
|
+
- **Comentários copiosos** em comandos destrutivos: `drop table`, `drop column`, `alter table ... drop column`, `truncate`, `delete from` em massa. Comentário explica o porquê + impacto.
|
|
24
|
+
- **`RLS` obrigatório em toda nova tabela** — `alter table public.<name> enable row level security;` no mesmo arquivo da criação.
|
|
25
|
+
- **`granular policies`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all`.
|
|
26
|
+
- **`(select auth.uid())`** sempre wrapped (REGRA #1 de RLS).
|
|
27
|
+
- **Index nas colunas referenciadas por RLS:** `create index on public.<table> (user_id);` no mesmo arquivo.
|
|
28
|
+
- Idempotência onde possível: `create table if not exists`, `create index if not exists`. Migrations rodam em ordem mas tooling pode re-executar.
|
|
29
|
+
- Migrations são **append-only**. Para reverter, criar nova migration que desfaz — nunca editar migration já aplicada.
|
|
30
|
+
|
|
31
|
+
## Patterns canônicos
|
|
32
|
+
|
|
33
|
+
### Criar tabela com RLS + policies granulares + index
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
/*
|
|
37
|
+
Migration: create_tasks
|
|
38
|
+
Created: 2026-05-06
|
|
39
|
+
Purpose: Cria tabela tasks com RLS habilitado e policies granulares por operação.
|
|
40
|
+
Affects: public.tasks (new), public.tasks policies (new — 4 policies)
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
create table if not exists public.tasks (
|
|
44
|
+
id uuid primary key default gen_random_uuid(),
|
|
45
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
46
|
+
title text not null,
|
|
47
|
+
status text not null default 'todo',
|
|
48
|
+
created_at timestamptz not null default now(),
|
|
49
|
+
updated_at timestamptz not null default now()
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
alter table public.tasks enable row level security;
|
|
53
|
+
|
|
54
|
+
-- granular policies: uma por operação por role
|
|
55
|
+
create policy "users_select_own_tasks"
|
|
56
|
+
on public.tasks for select to authenticated
|
|
57
|
+
using ((select auth.uid()) = user_id);
|
|
58
|
+
|
|
59
|
+
create policy "users_insert_own_tasks"
|
|
60
|
+
on public.tasks for insert to authenticated
|
|
61
|
+
with check ((select auth.uid()) = user_id);
|
|
62
|
+
|
|
63
|
+
create policy "users_update_own_tasks"
|
|
64
|
+
on public.tasks for update to authenticated
|
|
65
|
+
using ((select auth.uid()) = user_id)
|
|
66
|
+
with check ((select auth.uid()) = user_id);
|
|
67
|
+
|
|
68
|
+
create policy "users_delete_own_tasks"
|
|
69
|
+
on public.tasks for delete to authenticated
|
|
70
|
+
using ((select auth.uid()) = user_id);
|
|
71
|
+
|
|
72
|
+
-- index obrigatório nas colunas usadas pela policy
|
|
73
|
+
create index if not exists tasks_user_id_idx on public.tasks (user_id);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Adicionar coluna a tabela existente
|
|
77
|
+
|
|
78
|
+
```sql
|
|
79
|
+
/*
|
|
80
|
+
Migration: add_priority_to_tasks
|
|
81
|
+
Created: 2026-05-06
|
|
82
|
+
Purpose: Adiciona coluna priority (low/medium/high) a tasks com default low.
|
|
83
|
+
Affects: public.tasks (column added — non-destructive)
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
alter table public.tasks
|
|
87
|
+
add column if not exists priority text not null default 'low';
|
|
88
|
+
|
|
89
|
+
-- check constraint para enum-like
|
|
90
|
+
alter table public.tasks
|
|
91
|
+
add constraint tasks_priority_check
|
|
92
|
+
check (priority in ('low', 'medium', 'high'));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Operação destrutiva — drop column com comentário extensivo
|
|
96
|
+
|
|
97
|
+
```sql
|
|
98
|
+
/*
|
|
99
|
+
Migration: drop_legacy_subtitle_column
|
|
100
|
+
Created: 2026-05-06
|
|
101
|
+
Purpose: Remove coluna subtitle (deprecated em v3.0 — nunca foi usada em produção).
|
|
102
|
+
Affects: public.tasks (column dropped — DESTRUCTIVE)
|
|
103
|
+
Risk: Baixo — coluna nullable nunca populada (validado via select count(*) where subtitle is not null = 0).
|
|
104
|
+
Rollback: criar nova migration `add subtitle column` se necessário.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
-- DROP de coluna deprecated. Validado upstream: zero linhas com valor não-null.
|
|
108
|
+
-- Operação destrutiva — irreversível sem backup.
|
|
109
|
+
alter table public.tasks
|
|
110
|
+
drop column if exists subtitle;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Anti-patterns
|
|
114
|
+
|
|
115
|
+
### Anti-pattern 1: Criar tabela sem RLS
|
|
116
|
+
|
|
117
|
+
**Errado:**
|
|
118
|
+
```sql
|
|
119
|
+
create table public.tasks (
|
|
120
|
+
id uuid primary key default gen_random_uuid(),
|
|
121
|
+
user_id uuid not null,
|
|
122
|
+
title text not null
|
|
123
|
+
);
|
|
124
|
+
-- esqueceu enable row level security
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Por quê:** sem RLS, tabela exposta ao role `anon` e `authenticated` sem filtro — qualquer cliente lê tudo. RLS habilitado sem policies bloqueia tudo (mais seguro como default que deixar aberto).
|
|
128
|
+
|
|
129
|
+
**Certo:** sempre `alter table public.tasks enable row level security;` + policies granulares no mesmo arquivo.
|
|
130
|
+
|
|
131
|
+
### Anti-pattern 2: `for all` em vez de granular policies
|
|
132
|
+
|
|
133
|
+
**Errado:**
|
|
134
|
+
```sql
|
|
135
|
+
create policy "users_manage_tasks" on public.tasks
|
|
136
|
+
for all to authenticated
|
|
137
|
+
using ((select auth.uid()) = user_id);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Por quê:** mistura `using` (controla SELECT/UPDATE/DELETE) com `with check` (controla INSERT/UPDATE) — em UPDATE você pode querer regras diferentes para "qual linha tocar" vs "qual estado novo".
|
|
141
|
+
|
|
142
|
+
**Certo:** 4 policies separadas (uma por operação) — ver pattern "Criar tabela" acima.
|
|
143
|
+
|
|
144
|
+
### Anti-pattern 3: `drop column` sem comentário
|
|
145
|
+
|
|
146
|
+
**Errado:**
|
|
147
|
+
```sql
|
|
148
|
+
alter table public.tasks drop column legacy_field;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Por quê:** futuros leitores não sabem por que a coluna foi removida; rollback fica difícil; risk não documentado.
|
|
152
|
+
|
|
153
|
+
**Certo:** comentário no header explica Purpose + Affects + Risk + Rollback (ver pattern destrutivo acima).
|
|
154
|
+
|
|
155
|
+
### Anti-pattern 4: `auth.uid()` sem `(select)` wrapper
|
|
156
|
+
|
|
157
|
+
**Errado:**
|
|
158
|
+
```sql
|
|
159
|
+
using (auth.uid() = user_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Por quê:** degradação 1000× em queries com filtro RLS (Postgres reavalia por linha).
|
|
163
|
+
|
|
164
|
+
**Certo:**
|
|
165
|
+
```sql
|
|
166
|
+
using ((select auth.uid()) = user_id)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Ver também
|
|
170
|
+
|
|
171
|
+
- [supabase-postgres-style](../supabase-postgres-style/SKILL.md) — convenção de naming + style aplicada
|
|
172
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — granular policies + WARNING user_metadata
|
|
173
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''`
|
|
174
|
+
- [supabase-declarative-schema](../supabase-declarative-schema/SKILL.md) — workflow alternativo (declarative-first → diff)
|
|
175
|
+
- [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + comandos CLI
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-pgvector-rag
|
|
3
|
+
description: Use ao implementar RAG com pgvector — create extension vector, dim consistente, HNSW vs IVFFlat, operadores <=>/<#>, RAG with permissions via RLS, chunking.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — pgvector + RAG
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando implementar embeddings, similarity search ou RAG (Retrieval-Augmented Generation) com Supabase. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "pgvector", "vector embeddings"
|
|
13
|
+
- "RAG Supabase", "retrieval augmented generation"
|
|
14
|
+
- "semantic search Postgres"
|
|
15
|
+
- "HNSW vs IVFFlat"
|
|
16
|
+
- "embedding dimension"
|
|
17
|
+
- "match_documents function"
|
|
18
|
+
|
|
19
|
+
## Regras absolutas
|
|
20
|
+
|
|
21
|
+
- **Setup:** `create extension if not exists vector;` em migration ou `supabase/schemas/`.
|
|
22
|
+
- **Dimension fixa por modelo** — defina `embedding vector(N)` com N = dim do modelo: 1536 (OpenAI ada-002), 768 (nomic-embed-text), 384 (all-MiniLM-L6-v2). Mismatch = silent fail ou matches aleatórios.
|
|
23
|
+
- **Index obrigatório** — sem index, similarity search faz full scan e degrada drasticamente em > 10k linhas.
|
|
24
|
+
- **`HNSW`** = default em 2026 — recall melhor, queries mais rápidas com mais data. Build mais lento.
|
|
25
|
+
- **`IVFFlat`** = alternativa quando build time domina (datasets dinâmicos com re-build frequente). Recall menor.
|
|
26
|
+
- **Distance operators canônicos:**
|
|
27
|
+
- **`<=>`** — cosine distance (mais comum em embeddings normalizados)
|
|
28
|
+
- **`<#>`** — negative inner product
|
|
29
|
+
- **`<->`** — L2 (euclidean) distance
|
|
30
|
+
- **`RAG with permissions`** — combine similarity search com RLS na tabela source. Sem isso, retrieval vaza documents entre tenants.
|
|
31
|
+
- **Chunking:** 200-500 tokens com overlap 10-20%. Chunks > 1k tokens degradam embeddings; < 100 perdem contexto.
|
|
32
|
+
- **Embedding generation server-side** — geração via Edge Function ou worker (model API key não vai para client).
|
|
33
|
+
|
|
34
|
+
## Patterns canônicos
|
|
35
|
+
|
|
36
|
+
### Schema com RLS + HNSW index
|
|
37
|
+
|
|
38
|
+
```sql
|
|
39
|
+
-- PT-BR: extension uma vez por projeto
|
|
40
|
+
create extension if not exists vector;
|
|
41
|
+
|
|
42
|
+
-- PT-BR: documents com embedding e user_id para RAG with permissions
|
|
43
|
+
create table public.documents (
|
|
44
|
+
id uuid primary key default gen_random_uuid(),
|
|
45
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
46
|
+
content text not null, -- PT-BR: chunk de texto (200-500 tokens)
|
|
47
|
+
embedding vector(1536) not null, -- PT-BR: dim casa com modelo
|
|
48
|
+
metadata jsonb default '{}'::jsonb,
|
|
49
|
+
created_at timestamptz default now()
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- PT-BR: HNSW com cosine distance (default 2026)
|
|
53
|
+
create index documents_embedding_hnsw_idx
|
|
54
|
+
on public.documents
|
|
55
|
+
using hnsw (embedding vector_cosine_ops);
|
|
56
|
+
|
|
57
|
+
-- PT-BR: RLS — RAG with permissions
|
|
58
|
+
alter table public.documents enable row level security;
|
|
59
|
+
|
|
60
|
+
create policy "users_read_own_docs"
|
|
61
|
+
on public.documents for select to authenticated
|
|
62
|
+
using ((select auth.uid()) = user_id);
|
|
63
|
+
|
|
64
|
+
create policy "users_insert_own_docs"
|
|
65
|
+
on public.documents for insert to authenticated
|
|
66
|
+
with check ((select auth.uid()) = user_id);
|
|
67
|
+
|
|
68
|
+
create index documents_user_id_idx on public.documents (user_id);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Função `match_documents` — RAG with permissions
|
|
72
|
+
|
|
73
|
+
```sql
|
|
74
|
+
create or replace function public.match_documents(
|
|
75
|
+
query_embedding vector(1536),
|
|
76
|
+
match_threshold float,
|
|
77
|
+
match_count int
|
|
78
|
+
)
|
|
79
|
+
returns table (
|
|
80
|
+
id uuid,
|
|
81
|
+
content text,
|
|
82
|
+
similarity float,
|
|
83
|
+
metadata jsonb
|
|
84
|
+
)
|
|
85
|
+
language plpgsql
|
|
86
|
+
security invoker -- PT-BR: invoker → respeita RLS do caller
|
|
87
|
+
set search_path = ''
|
|
88
|
+
stable
|
|
89
|
+
as $$
|
|
90
|
+
begin
|
|
91
|
+
return query
|
|
92
|
+
select
|
|
93
|
+
d.id,
|
|
94
|
+
d.content,
|
|
95
|
+
1 - (d.embedding <=> query_embedding) as similarity,
|
|
96
|
+
d.metadata
|
|
97
|
+
from public.documents as d
|
|
98
|
+
where 1 - (d.embedding <=> query_embedding) > match_threshold
|
|
99
|
+
order by d.embedding <=> query_embedding
|
|
100
|
+
limit match_count;
|
|
101
|
+
end;
|
|
102
|
+
$$;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Por que `security invoker`:** garante que a função só retorna documentos que o caller (usuário autenticado) tem permissão de ver via RLS. Sem RLS, qualquer caller via similarity recupera documents de outros tenants.
|
|
106
|
+
|
|
107
|
+
### Uso da função do client
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// PT-BR: gerar embedding (em Edge Function, server-side) e chamar match_documents
|
|
111
|
+
const queryEmbedding = await embedQuery(userQuestion) // dim 1536
|
|
112
|
+
|
|
113
|
+
const { data: matches } = await supabase.rpc('match_documents', {
|
|
114
|
+
query_embedding: queryEmbedding,
|
|
115
|
+
match_threshold: 0.78,
|
|
116
|
+
match_count: 10,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// matches: [{ id, content, similarity, metadata }, ...] já filtrado por RLS
|
|
120
|
+
const context = matches.map((m) => m.content).join('\n\n')
|
|
121
|
+
const answer = await llmComplete({ context, question: userQuestion })
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### IVFFlat alternativa
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
-- PT-BR: usar IVFFlat quando build time importa (dataset dinâmico)
|
|
128
|
+
-- lists = sqrt(N) é heurística para N total de linhas
|
|
129
|
+
create index documents_embedding_ivfflat_idx
|
|
130
|
+
on public.documents
|
|
131
|
+
using ivfflat (embedding vector_cosine_ops)
|
|
132
|
+
with (lists = 100); -- ajustar conforme volume
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Geração de embeddings em Edge Function
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// supabase/functions/embed-document/index.ts
|
|
139
|
+
// PT-BR: chunking + embedding + insert
|
|
140
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
141
|
+
import OpenAI from 'npm:openai@4'
|
|
142
|
+
|
|
143
|
+
Deno.serve(async (req) => {
|
|
144
|
+
const { user_id, document } = await req.json()
|
|
145
|
+
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') })
|
|
146
|
+
const supabase = createClient(
|
|
147
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
148
|
+
Deno.env.get('SUPABASE_SECRET_KEYS')!
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// PT-BR: chunk em pedaços de ~400 tokens com overlap 20%
|
|
152
|
+
const chunks = chunkText(document, { size: 400, overlap: 80 })
|
|
153
|
+
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
const embedRes = await openai.embeddings.create({
|
|
156
|
+
model: 'text-embedding-3-small', // dim 1536
|
|
157
|
+
input: chunk,
|
|
158
|
+
})
|
|
159
|
+
await supabase.from('documents').insert({
|
|
160
|
+
user_id,
|
|
161
|
+
content: chunk,
|
|
162
|
+
embedding: embedRes.data[0].embedding,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return new Response('embedded')
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Anti-patterns
|
|
171
|
+
|
|
172
|
+
### Anti-pattern 1: Dim mismatch entre modelo e coluna
|
|
173
|
+
|
|
174
|
+
**Errado:**
|
|
175
|
+
```sql
|
|
176
|
+
create table documents (embedding vector(1536) not null);
|
|
177
|
+
```
|
|
178
|
+
```ts
|
|
179
|
+
// PT-BR: usa nomic-embed-text que retorna dim 768
|
|
180
|
+
const embedding = await nomicEmbed(text) // 768
|
|
181
|
+
await supabase.from('documents').insert({ embedding }) // ⚠ insert falha
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Por quê:** Postgres rejeita insert com dim mismatch (`expected 1536 dimensions, got 768`). Em pior caso, se aceito, similarity retorna ranking aleatório.
|
|
185
|
+
|
|
186
|
+
**Certo:** alinhe dim:
|
|
187
|
+
```sql
|
|
188
|
+
create table documents (embedding vector(768) not null);
|
|
189
|
+
```
|
|
190
|
+
ou troque o modelo para um que retorne 1536.
|
|
191
|
+
|
|
192
|
+
### Anti-pattern 2: Similarity search sem RLS — vazamento RAG
|
|
193
|
+
|
|
194
|
+
**Errado:**
|
|
195
|
+
```sql
|
|
196
|
+
-- PT-BR: tabela documents sem RLS
|
|
197
|
+
create table public.documents (
|
|
198
|
+
id uuid primary key,
|
|
199
|
+
content text,
|
|
200
|
+
embedding vector(1536)
|
|
201
|
+
);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Por quê:** qualquer authenticated chama `match_documents` e recupera documents de outros usuários. Em apps multi-tenant, é vazamento crítico — RAG do tenant A vê docs do tenant B.
|
|
205
|
+
|
|
206
|
+
**Certo:** habilite RLS + policies por `user_id`/`org_id` + use `security invoker` em funções de match (ver pattern canônico).
|
|
207
|
+
|
|
208
|
+
### Anti-pattern 3: Chunks gigantes (> 1k tokens)
|
|
209
|
+
|
|
210
|
+
**Errado:**
|
|
211
|
+
```ts
|
|
212
|
+
// PT-BR: chunk inteiro de documento (5k tokens)
|
|
213
|
+
const chunks = [fullDocument]
|
|
214
|
+
const embedding = await embed(fullDocument)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Por quê:** embeddings perdem detalhe semântico em chunks muito grandes. O modelo média muitos conceitos em um único vetor, similarity vira ruidosa. Ranking RAG fica ruim.
|
|
218
|
+
|
|
219
|
+
**Certo:** chunks de 200-500 tokens com overlap:
|
|
220
|
+
```ts
|
|
221
|
+
const chunks = chunkText(fullDocument, { size: 400, overlap: 80 })
|
|
222
|
+
for (const chunk of chunks) {
|
|
223
|
+
await embed(chunk).then(insert)
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Anti-pattern 4: Sem index em coluna `embedding`
|
|
228
|
+
|
|
229
|
+
**Errado:**
|
|
230
|
+
```sql
|
|
231
|
+
create table documents (embedding vector(1536) not null);
|
|
232
|
+
-- (esqueceu create index)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Por quê:** sem index, similarity search vira sequential scan. Em > 10k linhas, queries levam segundos a minutos. Em produção, app fica inviável.
|
|
236
|
+
|
|
237
|
+
**Certo:**
|
|
238
|
+
```sql
|
|
239
|
+
create index documents_embedding_hnsw_idx on documents using hnsw (embedding vector_cosine_ops);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Notas de futuro
|
|
243
|
+
|
|
244
|
+
- **Hybrid search** (FTS + vector com RRF — Reciprocal Rank Fusion) está coberto em skill `supabase-fts` (defer v1.9 — full-text search standalone).
|
|
245
|
+
- **Vector Buckets** e **Analytics Buckets** ainda em alpha em 2026 — mencione como existência mas não detalhe (pattern canônico evoluindo).
|
|
246
|
+
|
|
247
|
+
## Ver também
|
|
248
|
+
|
|
249
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — RLS é base de RAG with permissions
|
|
250
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — função `match_documents` com `security invoker` + `set search_path = ''`
|
|
251
|
+
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — geração de embeddings server-side
|
|
252
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — schema com `vector` extension
|
|
253
|
+
- [glossário](../_shared-supabase/glossary.md) — operadores `<=>`/`<#>`/`<->`
|