@luanpdd/kit-mcp 1.6.1 → 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 +126 -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/advisor-researcher.md +1 -14
- package/kit/agents/assumptions-analyzer.md +1 -14
- package/kit/agents/codebase-mapper.md +2 -15
- package/kit/agents/debugger.md +1 -19
- package/kit/agents/executor.md +18 -18
- package/kit/agents/integration-checker.md +1 -16
- package/kit/agents/nyquist-auditor.md +1 -16
- package/kit/agents/phase-researcher.md +1 -14
- package/kit/agents/plan-checker.md +1 -16
- package/kit/agents/planner.md +36 -16
- package/kit/agents/project-researcher.md +2 -15
- package/kit/agents/research-synthesizer.md +1 -9
- package/kit/agents/roadmapper.md +1 -14
- 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/ui-auditor.md +1 -16
- package/kit/agents/ui-checker.md +1 -16
- package/kit/agents/ui-researcher.md +1 -14
- package/kit/agents/user-profiler.md +2 -10
- package/kit/agents/verifier.md +2 -17
- package/kit/commands/depurar.md +17 -0
- package/kit/commands/expresso.md +9 -0
- package/kit/commands/fazer.md +32 -4
- package/kit/commands/proximo.md +7 -0
- package/kit/commands/rapido.md +6 -0
- package/kit/commands/supabase.md +148 -0
- package/kit/framework/references/output-style.md +22 -0
- package/kit/framework/workflows/discuss-phase.md +62 -327
- package/kit/framework/workflows/help.md +14 -1
- package/kit/framework/workflows/new-project.md +16 -107
- package/kit/framework/workflows/plan-phase.md +53 -147
- 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
- package/src/core/kit.js +55 -22
- package/src/core/sync.js +3 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-auth-ssr
|
|
3
|
+
description: Use ao bootstrap Next.js v16 + Supabase Auth — @supabase/ssr, getAll/setAll APENAS, NUNCA auth-helpers-nextjs, proxy completo com getUser e redirects.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Auth SSR (Next.js v16+)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando bootstrap ou auditar autenticação Supabase em Next.js v16+ (App Router) com SSR. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "Next.js + Supabase auth"
|
|
13
|
+
- "@supabase/ssr"
|
|
14
|
+
- "createServerClient", "createBrowserClient"
|
|
15
|
+
- "middleware.ts auth", "proxy auth"
|
|
16
|
+
- "cookies getAll setAll"
|
|
17
|
+
- "Supabase auth Next.js v16"
|
|
18
|
+
|
|
19
|
+
## Regras absolutas
|
|
20
|
+
|
|
21
|
+
**WARNING — NEVER use auth-helpers-nextjs.** O pacote `@supabase/auth-helpers-nextjs` está **DEPRECATED** e **quebra em Next.js v16+** (cookies API mudou). **SEMPRE** use `@supabase/ssr`.
|
|
22
|
+
|
|
23
|
+
**Outras regras:**
|
|
24
|
+
|
|
25
|
+
- **Padrão exclusivo `getAll`/`setAll`** para cookies — **NUNCA** `get`/`set`/`remove` individuais. Os métodos individuais não funcionam corretamente com middleware/Server Actions em Next.js v16+.
|
|
26
|
+
- **Browser client e Server client são distintos:**
|
|
27
|
+
- Browser (`createBrowserClient`) → para Client Components ("use client")
|
|
28
|
+
- Server (`createServerClient`) → para Server Components, Route Handlers, Server Actions
|
|
29
|
+
- **Middleware (`middleware.ts`) obrigatório** para refresh de sessão SSR. Deve chamar `supabase.auth.getUser()` em cada request.
|
|
30
|
+
- **Auth method order** — após `createServerClient` mas **ANTES** de `getUser()`, NÃO chamar nada que produza response intermediário. Os cookies precisam fluir corretamente.
|
|
31
|
+
- **`NEXT_PUBLIC_*` apenas para anon key** (`NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`). **NUNCA** `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY` — service_role bypassa RLS e seria exposto ao cliente (anti-pitfall B6).
|
|
32
|
+
- **Single serverClient factory** — não criar múltiplos clients em layouts (race condition na refresh de token — B13).
|
|
33
|
+
|
|
34
|
+
## Patterns canônicos
|
|
35
|
+
|
|
36
|
+
### Browser client — Client Components
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// utils/supabase/client.ts — PT-BR: client para Client Components
|
|
40
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
41
|
+
|
|
42
|
+
export function createClient() {
|
|
43
|
+
return createBrowserClient(
|
|
44
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
45
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// PT-BR: uso em Client Component
|
|
52
|
+
'use client'
|
|
53
|
+
import { createClient } from '@/utils/supabase/client'
|
|
54
|
+
|
|
55
|
+
export function LogoutButton() {
|
|
56
|
+
const supabase = createClient()
|
|
57
|
+
return <button onClick={() => supabase.auth.signOut()}>Sair</button>
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Server client — Server Components / Server Actions
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// utils/supabase/server.ts — PT-BR: client para Server Components/Actions/Route Handlers
|
|
65
|
+
import { createServerClient } from '@supabase/ssr'
|
|
66
|
+
import { cookies } from 'next/headers'
|
|
67
|
+
|
|
68
|
+
export async function createClient() {
|
|
69
|
+
const cookieStore = await cookies()
|
|
70
|
+
|
|
71
|
+
return createServerClient(
|
|
72
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
73
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
74
|
+
{
|
|
75
|
+
cookies: {
|
|
76
|
+
getAll() {
|
|
77
|
+
return cookieStore.getAll()
|
|
78
|
+
},
|
|
79
|
+
setAll(cookiesToSet) {
|
|
80
|
+
try {
|
|
81
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
82
|
+
cookieStore.set(name, value, options)
|
|
83
|
+
)
|
|
84
|
+
} catch {
|
|
85
|
+
// PT-BR: ok ignorar — chamado em Server Component (sem permissão de set)
|
|
86
|
+
// se proxy faz refresh, sessão fica saudável mesmo sem set aqui
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// PT-BR: uso em Server Component
|
|
97
|
+
import { createClient } from '@/utils/supabase/server'
|
|
98
|
+
|
|
99
|
+
export default async function Dashboard() {
|
|
100
|
+
const supabase = await createClient()
|
|
101
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
102
|
+
if (!user) return <p>Não autenticado</p>
|
|
103
|
+
return <p>Olá, {user.email}</p>
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Middleware — refresh de sessão (obrigatório)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// middleware.ts — PT-BR: proxy obrigatório para refresh de sessão SSR
|
|
111
|
+
import { createServerClient } from '@supabase/ssr'
|
|
112
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
113
|
+
|
|
114
|
+
export async function middleware(request: NextRequest) {
|
|
115
|
+
let supabaseResponse = NextResponse.next({ request })
|
|
116
|
+
|
|
117
|
+
const supabase = createServerClient(
|
|
118
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
119
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
120
|
+
{
|
|
121
|
+
cookies: {
|
|
122
|
+
getAll() {
|
|
123
|
+
return request.cookies.getAll()
|
|
124
|
+
},
|
|
125
|
+
setAll(cookiesToSet) {
|
|
126
|
+
cookiesToSet.forEach(({ name, value }) =>
|
|
127
|
+
request.cookies.set(name, value)
|
|
128
|
+
)
|
|
129
|
+
supabaseResponse = NextResponse.next({ request })
|
|
130
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
131
|
+
supabaseResponse.cookies.set(name, value, options)
|
|
132
|
+
)
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// PT-BR: ATENÇÃO — não execute código entre createServerClient e getUser()
|
|
139
|
+
// (qualquer cookie set/get fora desse path quebra refresh silencioso)
|
|
140
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
141
|
+
|
|
142
|
+
// PT-BR: redirect para /login se sem user
|
|
143
|
+
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
|
|
144
|
+
const url = request.nextUrl.clone()
|
|
145
|
+
url.pathname = '/login'
|
|
146
|
+
return NextResponse.redirect(url)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// PT-BR: IMPORTANTE — sempre retornar supabaseResponse (cookies precisam fluir)
|
|
150
|
+
return supabaseResponse
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const config = {
|
|
154
|
+
matcher: [
|
|
155
|
+
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Login com email/senha (Server Action)
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// app/login/actions.ts
|
|
164
|
+
'use server'
|
|
165
|
+
import { createClient } from '@/utils/supabase/server'
|
|
166
|
+
import { redirect } from 'next/navigation'
|
|
167
|
+
|
|
168
|
+
export async function loginAction(formData: FormData) {
|
|
169
|
+
const supabase = await createClient()
|
|
170
|
+
const { error } = await supabase.auth.signInWithPassword({
|
|
171
|
+
email: formData.get('email') as string,
|
|
172
|
+
password: formData.get('password') as string,
|
|
173
|
+
})
|
|
174
|
+
if (error) return { error: error.message }
|
|
175
|
+
redirect('/dashboard')
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Anti-patterns
|
|
180
|
+
|
|
181
|
+
### Anti-pattern 1: Importar de `@supabase/auth-helpers-nextjs`
|
|
182
|
+
|
|
183
|
+
**Errado:**
|
|
184
|
+
```ts
|
|
185
|
+
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
|
|
186
|
+
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Por quê:** `@supabase/auth-helpers-nextjs` está **DEPRECATED**. Quebra em Next.js v16+ (cookies API mudou). Não recebe mais updates de segurança.
|
|
190
|
+
|
|
191
|
+
**Certo:**
|
|
192
|
+
```ts
|
|
193
|
+
import { createServerClient, createBrowserClient } from '@supabase/ssr'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Anti-pattern 2: `cookies: { get, set, remove }` (individual)
|
|
197
|
+
|
|
198
|
+
**Errado:**
|
|
199
|
+
```ts
|
|
200
|
+
{
|
|
201
|
+
cookies: {
|
|
202
|
+
get(name: string) { return cookieStore.get(name) },
|
|
203
|
+
set(name: string, value: string) { cookieStore.set(name, value) },
|
|
204
|
+
remove(name: string) { cookieStore.remove(name) },
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Por quê:** cookie methods individuais quebram em middleware quando há múltiplos cookies sendo set/get em uma única request. `getAll`/`setAll` são chamados em batch e preservam ordem.
|
|
210
|
+
|
|
211
|
+
**Certo:** ver pattern "Server client" e "Middleware" acima.
|
|
212
|
+
|
|
213
|
+
### Anti-pattern 3: `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY`
|
|
214
|
+
|
|
215
|
+
**Errado:**
|
|
216
|
+
```bash
|
|
217
|
+
# .env.local
|
|
218
|
+
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJhbG...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Por quê:** `NEXT_PUBLIC_*` é **público** no client bundle. `service_role` bypassa RLS — vazamento = banco totalmente exposto.
|
|
222
|
+
|
|
223
|
+
**Certo:**
|
|
224
|
+
```bash
|
|
225
|
+
# .env.local
|
|
226
|
+
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
|
227
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG... # ok público
|
|
228
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJhbG... # privado — sem NEXT_PUBLIC_
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Use `service_role` apenas em código server-side que NUNCA é embarcado no bundle client (Route Handlers, Server Actions com `'use server'`, Edge Functions).
|
|
232
|
+
|
|
233
|
+
### Anti-pattern 4: Múltiplos serverClient em layouts (race condition)
|
|
234
|
+
|
|
235
|
+
**Errado:**
|
|
236
|
+
```tsx
|
|
237
|
+
// app/layout.tsx
|
|
238
|
+
const supabase1 = await createClient()
|
|
239
|
+
const { user } = await supabase1.auth.getUser()
|
|
240
|
+
// ...
|
|
241
|
+
// app/(dashboard)/layout.tsx
|
|
242
|
+
const supabase2 = await createClient() // ⚠ outro client na mesma request
|
|
243
|
+
const { user } = await supabase2.auth.getUser()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Por quê:** múltiplos `createServerClient` na mesma request podem corromper cookies de refresh de token. Issue [supabase/ssr#68](https://github.com/supabase/ssr/issues/68) — race condition documentada.
|
|
247
|
+
|
|
248
|
+
**Certo:** middleware faz o refresh **uma vez por request**. Layouts apenas leem o user via `getUser()` que retorna cached:
|
|
249
|
+
```tsx
|
|
250
|
+
// app/layout.tsx — middleware já fez o refresh
|
|
251
|
+
const supabase = await createClient()
|
|
252
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Ver também
|
|
256
|
+
|
|
257
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — RLS aplicado quando user autenticado consulta tabelas
|
|
258
|
+
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions usando service_role server-side
|
|
259
|
+
- [supabase-realtime](../supabase-realtime/SKILL.md) — Realtime exige usuário autenticado para canais privados
|
|
260
|
+
- [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + comandos CLI
|
|
@@ -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
|