@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/gates/agent-no-recursive-dispatch.md +48 -0
  3. package/gates/budget-description.md +68 -0
  4. package/gates/no-personal-uuid.md +72 -0
  5. package/gates/skill-must-include.md +69 -0
  6. package/gates/sync-idempotent.md +62 -0
  7. package/kit/agents/advisor-researcher.md +1 -14
  8. package/kit/agents/assumptions-analyzer.md +1 -14
  9. package/kit/agents/codebase-mapper.md +2 -15
  10. package/kit/agents/debugger.md +1 -19
  11. package/kit/agents/executor.md +18 -18
  12. package/kit/agents/integration-checker.md +1 -16
  13. package/kit/agents/nyquist-auditor.md +1 -16
  14. package/kit/agents/phase-researcher.md +1 -14
  15. package/kit/agents/plan-checker.md +1 -16
  16. package/kit/agents/planner.md +36 -16
  17. package/kit/agents/project-researcher.md +2 -15
  18. package/kit/agents/research-synthesizer.md +1 -9
  19. package/kit/agents/roadmapper.md +1 -14
  20. package/kit/agents/schema-checker.md +4 -4
  21. package/kit/agents/supabase-architect.md +153 -0
  22. package/kit/agents/supabase-auth-bootstrapper.md +298 -0
  23. package/kit/agents/supabase-edge-fn-writer.md +185 -0
  24. package/kit/agents/supabase-migration-writer.md +156 -0
  25. package/kit/agents/supabase-realtime-implementer.md +252 -0
  26. package/kit/agents/supabase-rls-writer.md +218 -0
  27. package/kit/agents/supabase-storage-implementer.md +240 -0
  28. package/kit/agents/ui-auditor.md +1 -16
  29. package/kit/agents/ui-checker.md +1 -16
  30. package/kit/agents/ui-researcher.md +1 -14
  31. package/kit/agents/user-profiler.md +2 -10
  32. package/kit/agents/verifier.md +2 -17
  33. package/kit/commands/depurar.md +17 -0
  34. package/kit/commands/expresso.md +9 -0
  35. package/kit/commands/fazer.md +32 -4
  36. package/kit/commands/proximo.md +7 -0
  37. package/kit/commands/rapido.md +6 -0
  38. package/kit/commands/supabase.md +148 -0
  39. package/kit/framework/references/output-style.md +22 -0
  40. package/kit/framework/workflows/discuss-phase.md +62 -327
  41. package/kit/framework/workflows/help.md +14 -1
  42. package/kit/framework/workflows/new-project.md +16 -107
  43. package/kit/framework/workflows/plan-phase.md +53 -147
  44. package/kit/skills/_shared-supabase/glossary.md +180 -0
  45. package/kit/skills/supabase-auth-ssr/SKILL.md +260 -0
  46. package/kit/skills/supabase-cron-queues/SKILL.md +266 -0
  47. package/kit/skills/supabase-database-functions/SKILL.md +247 -0
  48. package/kit/skills/supabase-declarative-schema/SKILL.md +183 -0
  49. package/kit/skills/supabase-edge-functions/SKILL.md +242 -0
  50. package/kit/skills/supabase-migrations/SKILL.md +175 -0
  51. package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -0
  52. package/kit/skills/supabase-postgres-style/SKILL.md +138 -0
  53. package/kit/skills/supabase-realtime/SKILL.md +236 -0
  54. package/kit/skills/supabase-rls-policies/SKILL.md +185 -0
  55. package/kit/skills/supabase-storage/SKILL.md +234 -0
  56. package/package.json +1 -1
  57. package/src/core/kit.js +55 -22
  58. package/src/core/sync.js +3 -1
@@ -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
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: supabase-storage
3
+ description: Use ao integrar Storage — buckets públicos vs privados, signed URLs com expiration, RLS sobre storage.objects com multi-tenant path, image transforms, TUS uploads.
4
+ ---
5
+
6
+ # Supabase — Storage
7
+
8
+ ## Quando usar
9
+
10
+ LLM carrega esta skill quando trabalhar com upload, download, ou serve de arquivos via Supabase Storage. Trigger phrases:
11
+
12
+ - "Supabase Storage", "upload de arquivo"
13
+ - "signed URL", "createSignedUrl"
14
+ - "bucket público vs privado"
15
+ - "RLS storage.objects"
16
+ - "multi-tenant arquivos"
17
+ - "image transforms Supabase"
18
+ - "TUS resumable upload"
19
+
20
+ ## Regras absolutas
21
+
22
+ - **Bucket privado é default em produção** — apenas dados públicos (avatares públicos, marketing) vão em buckets públicos.
23
+ - **Bucket público:** URL direta `getPublicUrl()` + servida via CDN (cache).
24
+ - **Bucket privado:** apenas `signed URL` (`createSignedUrl()`) com `expiresIn` curto (60s downloads, 3600s imagens).
25
+ - **`storage.objects`** — RLS sempre habilitada. Sem RLS, qualquer authenticated lê qualquer bucket privado.
26
+ - **`multi-tenant path`** isolation — usar `auth.uid()` (ou `org_id`) como path prefix: `<user_id>/<filename>`. Validar em RLS via `(storage.foldername(name))[1] = (select auth.uid())::text`.
27
+ - **Image transformations** apenas em buckets com transformation enabled (Pro plan+). Query params `?width=800&height=600&resize=contain`.
28
+ - **Uploads grandes (> 6 MB):** use TUS resumable protocol (`uploadToSignedUrl` + chunked upload).
29
+ - **Awareness de egress billing** — bucket público sem cache headers customizados pode disparar custo significativo. Use Smart CDN + TTL adequado.
30
+ - **Não overwrite arquivos públicos** com mesmo nome — CDN cache fica stale. Use versionamento (`avatar-v2.jpg`) ou random suffix.
31
+
32
+ ## Patterns canônicos
33
+
34
+ ### RLS multi-tenant em `storage.objects`
35
+
36
+ ```sql
37
+ -- PT-BR: usuário só vê arquivos sob seu próprio prefix de path
38
+ -- path canônico: <user_id>/<filename> (ex: 550e8400-e29b-41d4-a716-446655440000/avatar.jpg)
39
+
40
+ create policy "users_read_own_files"
41
+ on storage.objects for select to authenticated
42
+ using (
43
+ bucket_id = 'private-uploads'
44
+ and (storage.foldername(name))[1] = (select auth.uid())::text
45
+ );
46
+
47
+ create policy "users_insert_own_files"
48
+ on storage.objects for insert to authenticated
49
+ with check (
50
+ bucket_id = 'private-uploads'
51
+ and (storage.foldername(name))[1] = (select auth.uid())::text
52
+ );
53
+
54
+ create policy "users_update_own_files"
55
+ on storage.objects for update to authenticated
56
+ using (
57
+ bucket_id = 'private-uploads'
58
+ and (storage.foldername(name))[1] = (select auth.uid())::text
59
+ );
60
+
61
+ create policy "users_delete_own_files"
62
+ on storage.objects for delete to authenticated
63
+ using (
64
+ bucket_id = 'private-uploads'
65
+ and (storage.foldername(name))[1] = (select auth.uid())::text
66
+ );
67
+ ```
68
+
69
+ ### Upload com path multi-tenant
70
+
71
+ ```ts
72
+ // PT-BR: cliente — path sempre prefixado com user.id
73
+ import { createClient } from '@/utils/supabase/client'
74
+
75
+ async function uploadAvatar(file: File) {
76
+ const supabase = createClient()
77
+ const { data: { user } } = await supabase.auth.getUser()
78
+ if (!user) throw new Error('not authenticated')
79
+
80
+ // PT-BR: path = <user_id>/<filename>
81
+ const path = `${user.id}/avatar.jpg`
82
+
83
+ const { data, error } = await supabase.storage
84
+ .from('private-uploads')
85
+ .upload(path, file, {
86
+ cacheControl: '3600',
87
+ upsert: true, // PT-BR: sobrescreve se mesmo path
88
+ })
89
+
90
+ if (error) throw error
91
+ return data
92
+ }
93
+ ```
94
+
95
+ ### Signed URL — download privado
96
+
97
+ ```ts
98
+ // PT-BR: signed URL com expiração 1h
99
+ const { data, error } = await supabase.storage
100
+ .from('private-uploads')
101
+ .createSignedUrl(`${userId}/avatar.jpg`, 3600)
102
+
103
+ // data.signedUrl pode ser usado em <img src={data.signedUrl}> por 1h
104
+ ```
105
+
106
+ ### Image transformations (em bucket com transform habilitado)
107
+
108
+ ```ts
109
+ // PT-BR: signed URL com transformação inline
110
+ const { data } = await supabase.storage
111
+ .from('private-uploads')
112
+ .createSignedUrl(`${userId}/avatar.jpg`, 3600, {
113
+ transform: { width: 200, height: 200, resize: 'cover' },
114
+ })
115
+ ```
116
+
117
+ ### Public bucket — getPublicUrl + cache headers
118
+
119
+ ```ts
120
+ // PT-BR: para bucket PÚBLICO apenas (não funciona em privado)
121
+ const { data } = supabase.storage
122
+ .from('public-avatars')
123
+ .getPublicUrl('hero.jpg')
124
+
125
+ // PT-BR: ao upload, set cacheControl alto para reduzir egress
126
+ await supabase.storage
127
+ .from('public-avatars')
128
+ .upload('hero.jpg', file, {
129
+ cacheControl: '31536000', // 1 ano — assets imutáveis
130
+ upsert: false, // não sobrescrever (versionar via path)
131
+ })
132
+ ```
133
+
134
+ ### TUS resumable upload (arquivos grandes)
135
+
136
+ ```ts
137
+ // PT-BR: signed upload URL + TUS chunked upload (>6MB)
138
+ import * as tus from 'npm:tus-js-client'
139
+
140
+ async function uploadLarge(file: File, path: string) {
141
+ const supabase = createClient()
142
+ const { data, error } = await supabase.storage
143
+ .from('private-uploads')
144
+ .createSignedUploadUrl(path)
145
+ if (error) throw error
146
+
147
+ return new Promise((resolve, reject) => {
148
+ const upload = new tus.Upload(file, {
149
+ endpoint: data.signedUrl,
150
+ headers: { authorization: `Bearer ${data.token}` },
151
+ chunkSize: 6 * 1024 * 1024, // 6 MB chunks
152
+ onError: reject,
153
+ onSuccess: () => resolve(upload.url),
154
+ })
155
+ upload.start()
156
+ })
157
+ }
158
+ ```
159
+
160
+ ### Notas de futuro (alpha — não detalhar em produção)
161
+
162
+ - **Vector Buckets** e **Analytics Buckets** existem em alpha (2026). Mencione apenas como existência se relevante; pattern canônico ainda mudando — não detalhar.
163
+
164
+ ## Anti-patterns
165
+
166
+ ### Anti-pattern 1: Path sem prefix de tenant
167
+
168
+ **Errado:**
169
+ ```ts
170
+ await supabase.storage.from('private-uploads').upload('avatar.jpg', file)
171
+ ```
172
+
173
+ **Por quê:** path global — qualquer user sobrescreve `avatar.jpg`. RLS multi-tenant não consegue isolar.
174
+
175
+ **Certo:**
176
+ ```ts
177
+ const path = `${user.id}/avatar.jpg`
178
+ await supabase.storage.from('private-uploads').upload(path, file)
179
+ ```
180
+
181
+ ### Anti-pattern 2: Bucket privado sem RLS em `storage.objects`
182
+
183
+ **Errado:**
184
+ ```sql
185
+ create bucket 'private-uploads';
186
+ -- (esqueceu policies em storage.objects)
187
+ ```
188
+
189
+ **Por quê:** sem RLS em `storage.objects`, qualquer `authenticated` lê arquivos do bucket — multi-tenancy quebrado.
190
+
191
+ **Certo:** ver pattern "RLS multi-tenant" acima — 4 policies separadas (SELECT/INSERT/UPDATE/DELETE).
192
+
193
+ ### Anti-pattern 3: `getPublicUrl` em bucket privado
194
+
195
+ **Errado:**
196
+ ```ts
197
+ // PT-BR: bucket-id é privado
198
+ const { data } = supabase.storage.from('private-uploads').getPublicUrl('x.jpg')
199
+ // data.publicUrl retorna mas o URL não funciona (403)
200
+ ```
201
+
202
+ **Por quê:** `getPublicUrl` só funciona em buckets marcados public. Em privado, retorna URL que sempre dá 403.
203
+
204
+ **Certo:** use `createSignedUrl` com expiration:
205
+ ```ts
206
+ const { data } = await supabase.storage
207
+ .from('private-uploads')
208
+ .createSignedUrl('x.jpg', 3600)
209
+ // data.signedUrl funciona por 1h
210
+ ```
211
+
212
+ ### Anti-pattern 4: Overwrite de arquivo público com mesmo path
213
+
214
+ **Errado:**
215
+ ```ts
216
+ // PT-BR: hero.jpg público
217
+ await supabase.storage.from('public').upload('hero.jpg', newFile, { upsert: true })
218
+ // CDN cache antigo continua servindo old hero.jpg por horas/dias
219
+ ```
220
+
221
+ **Por quê:** CDN cache pelo path. Overwrite não invalida cache; usuários veem versão antiga.
222
+
223
+ **Certo:** versionar ou random suffix:
224
+ ```ts
225
+ await supabase.storage.from('public').upload(`hero-${version}.jpg`, newFile)
226
+ // ou: hero-<sha>.jpg, hero-<timestamp>.jpg
227
+ ```
228
+
229
+ ## Ver também
230
+
231
+ - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) — RLS sobre `storage.objects` + multi-tenant pattern
232
+ - [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — usuário autenticado obtém `auth.uid()` para path prefix
233
+ - [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions podem mediar uploads complexos
234
+ - [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.6.1",
3
+ "version": "1.8.1",
4
4
  "description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
5
5
  "type": "module",
6
6
  "bin": {