Supabase Edge Functions com segurança
Runbook DevOps para Supabase Edge Functions: webhooks, assinaturas, CORS, Deno runtime, validação de entrada e integração com Claude Code.
Supabase Edge Functions são funções TypeScript do lado do servidor que rodam no Deno Runtime e são acessíveis pelo Kong API Gateway.
Elas são particularmente adequadas para webhooks, integrações com APIs externas, endpoints de eventos assinados e lógica leve do lado do servidor.
Estatística: De acordo com dados do Stripe, mais de 12% das integrações de webhooks não verificam assinaturas - tornando-as vulneráveis a eventos falsificados.
O erro mais comum é utilizar Edge Functions como um segundo sistema de backend. Isso leva a lógica de negócio duplicada, limites de segurança pouco claros e arquiteturas difíceis de debugar.
Este runbook descreve como Edge Functions são utilizadas com segurança no setup self-hosted. Cada passo contém uma implementação concreta com código Deno real, uma condição verificável e um cenário de falha.
Nota sobre o runtime: As Supabase Edge Functions são baseadas em Deno, não em Node.js. Isso afeta a sintaxe de import, o sistema de módulos e as APIs disponíveis. Todos os exemplos de código neste artigo são compatíveis com Deno. Edge Functions self-hosted rodam no container
supabase/edge-runtimee atualmente ainda são classificadas como Beta.
Resumo - Artigo 3 de 6 da série DevOps Runbook
- Edge Functions apenas como pontos de integração (webhooks), não segundo backend
- Verificação obrigatória de assinaturas de webhooks
- CORS configurado explicitamente (sem wildcard)
- Validação de entrada com Zod
- Tarefas de longa duração delegadas ao Trigger.dev
Sumário da série
Este guia faz parte da nossa série de runbooks DevOps para stacks de aplicações self-hosted.
- Supabase Self-Hosting Runbook
- Next.js sobre Supabase com segurança
- Supabase Edge Functions com segurança - este artigo
- Trigger.dev Background Jobs com segurança
- Claude Code como controle de segurança no workflow DevOps
- Security Baseline para todo o stack
O artigo 1 descreve a plataforma. O artigo 2 descreve a camada de aplicação Next.js. Este artigo descreve integrações e webhooks.
Visão geral da arquitetura
Browser
|
Next.js (camada de aplicação)
|
+-- Supabase Client (anon key) -> para requests de usuários
|
Kong API Gateway
|
+-- PostgREST -> REST API com RLS
+-- GoTrue -> Auth
+-- Edge Runtime -> Edge Functions
| |
| +-- stripe-webhook
| +-- github-webhook
| +-- trigger-webhook
|
PostgreSQL + RLS Policies
Serviços externos
|
+-- Stripe / GitHub / Trigger.dev -> chamam os webhooks
Regra fundamental:
Edge Functions são pontos de integração para eventos externos.
Lógica de negócio pertence ao [Next.js](/br/revista/nextjs-supabase-configuracao-segura/) (Server Actions / Route Handler).
Processos de longa duração pertencem ao [Trigger.dev](/br/revista/trigger-dev-background-jobs/).
Tabela de decisão
| Critério | Edge Function | Server Action | Trigger.dev Task |
|---|---|---|---|
| Origem | Evento externo (webhook) | Entrada do usuário | Tarefa interna |
| Tempo de execução | < 30 segundos | < 10 segundos | Até 5 minutos+ |
| Autenticação | Assinatura HMAC | Sessão do usuário (JWT) | Chave API / service_role |
| RLS | Ignorado (service_role) | Ativo (anon key) | Ignorado (service_role) |
| Retry | Provedor do webhook | Nenhum (síncrono) | Integrado (Trigger.dev) |
| Exemplo | Stripe checkout webhook | Formulário de perfil | Geração de PDF |
Critério de decisão
Quando uma função reage a um evento externo (Stripe Payment, GitHub Push, Trigger.dev Callback), ela é uma Edge Function. Quando ela reage a input de usuário e transforma dados, ela pertence ao Next.js. Quando ela roda por mais de 30 segundos, ela pertence ao Trigger.dev.
Como Edge Functions rodam no setup Self-Hosted
No stack Docker Compose self-hosted, a configuração das Edge Functions é a seguinte:
# docker-compose.yml (trecho)
functions:
container_name: supabase-edge-functions
image: supabase/edge-runtime:v1.66.4 # fixar versão
restart: unless-stopped
depends_on:
- analytics
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000 # hostname Docker interno
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
As Edge Functions ficam como arquivos TypeScript no volume:
volumes/functions/
main/
index.ts <- Router / Main Service
_shared/
cors.ts <- CORS Headers (compartilhado)
supabase-client.ts <- Supabase Client Factory (compartilhado)
stripe-webhook/
index.ts
github-webhook/
index.ts
Deployment no setup Self-Hosted: as funções são colocadas como arquivos no volume e o container é reiniciado:
# Copiar functions para o volume
cp -r supabase/functions/* /opt/supabase/volumes/functions/
# Reiniciar o container
docker compose restart functions --no-deps
Parte A - Decisões de arquitetura
Essas decisões são tomadas uma vez e raramente alteradas.
A1 - Edge Functions apenas para integrações, não como segundo backend
Implementação
Definição clara de qual lógica pertence a qual camada:
Edge Functions (Deno):
Recebimento de webhooks (Stripe, GitHub, APIs externas)
Sync baseado em eventos (DB Webhook -> serviço externo)
Verificação de assinatura de eventos recebidos
Transformações leves (< 30 segundos)
Next.js (Server Actions / Route Handler):
Mutations voltadas ao usuário (CRUD)
Operações baseadas em sessão
Lógica de negócio
Validação de input com contexto de usuário
Trigger.dev (Background Jobs):
Geração de PDF
Tarefas de IA
Envio de e-mails em massa
Tudo com tempo de execução acima de 30 segundos
Condição verificável
# Quantas Edge Functions existem?
ls -d volumes/functions/*/ | grep -v "main\|_shared" | wc -l
# Para cada Function verificar: é uma integração?
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
echo "--- $name ---"
# Contém patterns de Webhook/Integração?
grep -l "signature\|webhook\|stripe\|github\|trigger" "$dir"*.ts 2>/dev/null || \
echo "AVISO: nenhum pattern de Webhook/Integração encontrado"
done
Cenário de falha
Quando lógica de negócio é implementada em Edge Functions, surge uma arquitetura sombra: a mesma validação existe nas Server Actions do Next.js E nas Edge Functions, com diferenças sutis. Bugs se tornam difíceis de reproduzir, pois não fica claro qual caminho de código estava ativo. Lógica de autenticação precisa ser mantida em dois lugares.
A2 - Isolar endpoints de webhook: uma função por webhook
Implementação
Cada provedor de webhook recebe uma função própria em um diretório próprio:
volumes/functions/
stripe-webhook/
index.ts
github-webhook/
index.ts
trigger-webhook/
index.ts
Não faça assim:
volumes/functions/
webhooks/
index.ts <- processa Stripe, GitHub e Trigger em um único arquivo
Condição verificável
# Cada Webhook Function deve atender exatamente um provedor
for dir in volumes/functions/*-webhook/; do
name=$(basename "$dir")
providers=$(grep -ciE "stripe|github|trigger|slack|sendgrid" "$dir/index.ts")
if [ "$providers" -gt 1 ]; then
echo "AVISO: $name atende múltiplos provedores ($providers)"
fi
done
Cenário de falha
Quando múltiplos webhooks são processados em uma única função, eles compartilham o mesmo error handler. Um payload Stripe com erro pode bloquear o webhook do GitHub. Estratégias de retry diferem por provedor (Stripe usa exponential backoff, GitHub apenas 3x), o que dificilmente pode ser implementado de forma limpa em uma função compartilhada.
A3 - Configurar CORS corretamente
Implementação
Edge Functions chamadas pelo navegador (mesmo indiretamente) precisam de headers CORS. O Supabase não fornece configuração CORS automática para Edge Functions.
Configuração CORS compartilhada:
// volumes/functions/_shared/cors.ts
const allowedOrigins = [
'https://app.example.com',
...(Deno.env.get('ENVIRONMENT') === 'development'
? ['http://localhost:3000']
: [])
]
export function getCorsHeaders(req: Request) {
const origin = req.headers.get('origin') ?? ''
const corsOrigin = allowedOrigins.includes(origin) ? origin : ''
return {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
}
}
Cada Edge Function precisa tratar CORS no início:
import { getCorsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
// CORS Preflight deve ser a PRIMEIRA verificação
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// ... lógica principal
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
})
Condição verificável
# Todas as Functions precisam tratar CORS
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if ! grep -q "OPTIONS" "$dir/index.ts" 2>/dev/null; then
echo "AVISO: $name não tem handler OPTIONS"
fi
done
# Nenhum CORS wildcard em produção
grep -r "'\\*'" volumes/functions/ --include="*.ts" | grep -i "allow-origin"
# Esperado: nenhum resultado (exceto em branches de desenvolvimento)
Cenário de falha
Sem headers CORS, todos os requests do navegador para Edge Functions falham. O navegador bloqueia a response, mesmo que a função responda corretamente. Isso se manifesta como um erro CORS enigmático no console. Com CORS wildcard ('*') em produção, qualquer site pode enviar requests para suas Edge Functions e ler as responses.
Parte B - Verificações de implementação
Estas verificações valem para cada Edge Function e devem ser conferidas a cada deployment.
B1 - Verificar assinaturas de webhook
Implementação
Cada endpoint de webhook precisa verificar a assinatura do serviço remetente. Sem verificação de assinatura, qualquer pessoa pode enviar payloads arbitrários para a Function.
Exemplo completo de Stripe Webhook:
// volumes/functions/stripe-webhook/index.ts
import { getCorsHeaders } from '../_shared/cors.ts'
const STRIPE_WEBHOOK_SECRET = Deno.env.get('STRIPE_WEBHOOK_SECRET')
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// 1. Permitir apenas POST
if (req.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// 2. Verificar assinatura
const signature = req.headers.get('stripe-signature')
if (!signature) {
return new Response(
JSON.stringify({ error: 'Missing signature' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
const body = await req.text()
const isValid = await verifyStripeSignature(body, signature, STRIPE_WEBHOOK_SECRET!)
if (!isValid) {
return new Response(
JSON.stringify({ error: 'Invalid signature' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// 3. Processar evento
const event = JSON.parse(body)
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object)
break
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object)
break
default:
// Ignorar eventos desconhecidos, não falhar
console.log(`Unhandled event type: ${event.type}`)
}
// 4. Sempre retornar 200 (caso contrário o Stripe faz retry)
return new Response(
JSON.stringify({ received: true }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
// Verificação de assinatura Stripe com Deno Crypto API
async function verifyStripeSignature(
payload: string,
header: string,
secret: string
): Promise<boolean> {
const parts = header.split(',')
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1]
const signature = parts.find(p => p.startsWith('v1='))?.split('=')[1]
if (!timestamp || !signature) return false
// Timing Check: rejeitar eventos com mais de 5 minutos
const now = Math.floor(Date.now() / 1000)
if (now - parseInt(timestamp) > 300) return false
const signedPayload = `${timestamp}.${payload}`
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const sig = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signedPayload)
)
const expectedSig = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
return expectedSig === signature
}
async function handleCheckoutCompleted(session: Record<string, unknown>) {
console.log(`Checkout completed: ${session.id}`)
}
async function handlePaymentFailed(invoice: Record<string, unknown>) {
console.log(`Payment failed: ${invoice.id}`)
}
Verificação de assinatura de webhook do GitHub (procedimento diferente):
// volumes/functions/github-webhook/index.ts
const GITHUB_WEBHOOK_SECRET = Deno.env.get('GITHUB_WEBHOOK_SECRET')
async function verifyGitHubSignature(
payload: string,
signatureHeader: string,
secret: string
): Promise<boolean> {
// GitHub usa sha256=<hex>
const expected = signatureHeader.replace('sha256=', '')
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const sig = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(payload)
)
const computed = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
return computed === expected
}
Condição verificável
# Cada Webhook Function precisa ter verificação de assinatura
for dir in volumes/functions/*-webhook/; do
name=$(basename "$dir")
if ! grep -qE "signature|verify|hmac" "$dir/index.ts" 2>/dev/null; then
echo "CRITICO: $name não possui verificação de assinatura"
else
echo "OK: $name verifica assinaturas"
fi
done
# Verificar configuração VERIFY_JWT
# Webhooks precisam de VERIFY_JWT=false, pois serviços externos não enviam JWT do Supabase
grep "VERIFY_JWT" .env
Cenário de falha
Sem verificação de assinatura, um invasor pode enviar eventos arbitrários para o webhook:
curl -X POST https://app.example.com/functions/v1/stripe-webhook \
-H "Content-Type: application/json" \
-d '{"type":"checkout.session.completed","data":{"object":{"customer":"cus_fake"}}}'
Esse request dispararia uma confirmação de checkout falsificada. A função atualizaria o status do usuário, embora nunca tenha havido pagamento.
B2 - Inicializar Supabase Client corretamente (anon vs. service_role)
Implementação
Edge Functions têm acesso a todas as variáveis de ambiente do Supabase. A escolha entre a chave anon e a chave service_role é a decisão de segurança mais importante por Function.
Client Factory compartilhada:
// volumes/functions/_shared/supabase-client.ts
import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2'
// Client COM RLS (para operações com contexto de usuário)
export function createAnonClient(authHeader?: string): SupabaseClient {
const client = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: authHeader ? { Authorization: authHeader } : {},
},
}
)
return client
}
// Client SEM RLS (para processamento de webhook, tarefas admin)
// ATENÇÃO: ignora todas as Row Level Security Policies
export function createAdminClient(): SupabaseClient {
return createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
}
Quando usar qual client:
createAnonClient(authHeader):
Functions voltadas ao usuário (raro, geralmente pertencem ao Next.js)
Quando RLS deve controlar o acesso
createAdminClient():
Processamento de webhook (Stripe, GitHub)
Tarefas de sync internas
Quando a Function NÃO tem contexto de usuário
Regra geral: webhooks não têm contexto de usuário,
portanto precisam de service_role.
Mas: executar apenas as operações mínimas necessárias.
Condição verificável
# Onde service_role / Admin Client é utilizado?
grep -rn "SERVICE_ROLE\|createAdminClient\|service_role" \
volumes/functions/ --include="*.ts" | grep -v "_shared/"
# Esperado: apenas em Webhook Functions, não em Functions voltadas ao usuário
# O Admin Client recebe input de usuário não validado?
# Review manual: em cada Function que usa createAdminClient,
# verificar se o input é validado antes da operação no banco
Cenário de falha
Quando uma Edge Function roda com service_role e repassa input de usuário diretamente em queries, ela ignora o RLS completamente. Um payload de webhook manipulado poderia então ler ou escrever dados arbitrários em quaisquer tabelas, pois a chave service_role não possui restrições.
B3 - Validação de entrada
Implementação
Cada Edge Function precisa validar os dados recebidos antes de processá-los. No Deno, o Zod funciona via import npm:
// volumes/functions/stripe-webhook/index.ts (trecho)
import { z } from 'npm:zod@3'
// Schema para o evento Stripe esperado
const stripeEventSchema = z.object({
id: z.string().startsWith('evt_'),
type: z.string(),
data: z.object({
object: z.record(z.unknown()),
}),
})
// Na função principal após a verificação de assinatura:
const parsed = stripeEventSchema.safeParse(JSON.parse(body))
if (!parsed.success) {
return new Response(
JSON.stringify({ error: 'Invalid payload structure' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// A partir daqui trabalhar com parsed.data (type-safe)
const event = parsed.data
Condição verificável
# Cada Function deve ter validação de entrada
for dir in volumes/functions/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if grep -qE "zod|safeParse|z\.object|z\.string" "$dir/index.ts" 2>/dev/null; then
echo "OK: $name possui Schema Validation"
elif grep -qE "JSON\.parse" "$dir/index.ts" 2>/dev/null; then
echo "AVISO: $name faz parse de JSON sem Schema Validation"
fi
done
Cenário de falha
Sem validação de entrada, a Function aceita qualquer payload que tenha uma assinatura válida. Uma chave de API comprometida no provedor de webhook poderia então enviar estruturas de dados inesperadas, levando a operações de banco de dados indefinidas, por exemplo undefined como User-ID em um insert.
B4 - Secrets em variáveis de ambiente, nunca no código
Implementação
No setup Self-Hosted, os secrets são passados para as Edge Functions pela configuração do Docker Compose:
# docker-compose.yml (trecho)
functions:
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
env_file:
- .env.functions # secrets adicionais para Functions
# .env.functions (apenas no servidor, não no Git)
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_SECRET_KEY=sk_live_...
GITHUB_WEBHOOK_SECRET=ghsec_...
TRIGGER_DEV_API_KEY=tr_...
Acessar no código:
// Assim: variável de ambiente
const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET')
// NUNCA assim: hardcoded
const secret = 'whsec_abc123...'
Para os fundamentos gerais de gerenciamento de secrets, consulte Segurança de dados na infraestrutura enterprise de IA.
Condição verificável
# Secrets hardcoded no código?
grep -rn "sk_live\|sk_test\|whsec_\|ghsec_\|Bearer ey" \
volumes/functions/ --include="*.ts"
# Esperado: nenhum resultado
# .env.functions não está no Git?
git ls-files .env.functions
# Esperado: vazio
# Permissões de arquivo corretas?
stat -c "%a" .env.functions
# Esperado: 600
Cenário de falha
Secrets hardcoded no código-fonte acabam no repositório Git. Mesmo que o repositório seja privado, todos os desenvolvedores com acesso ao repo também têm acesso aos secrets de produção. Em um push acidental para público, os secrets ficam imediatamente comprometidos.
B5 - Evitar timeouts e processos de longa duração
Implementação
Edge Functions são projetadas para operações curtas e idempotentes. O Edge Runtime Self-Hosted possui um timeout padrão (configurável, tipicamente 60 segundos para self-hosted). Processos de longa duração bloqueiam slots de worker.
Adequado (< 30 segundos):
Receber webhook e atualizar o banco
Verificar assinatura e encaminhar evento
Chamadas curtas de API para serviços externos
NÃO adequado (usar Trigger.dev):
Geração de PDF
Inferência de IA (chamadas LLM)
Processamento de vídeo/imagem
Operações de banco de dados em massa
Envio de e-mail para muitos destinatários
Pattern para processos longos: Edge Function como trigger, job no Trigger.dev:
// volumes/functions/process-document/index.ts
// CORRETO: Edge Function apenas dispara o job
Deno.serve(async (req) => {
// ... verificar assinatura, validar input ...
// Delegar job de longa duração ao Trigger.dev
const triggerResponse = await fetch(
`${Deno.env.get('TRIGGER_DEV_URL')}/api/v1/tasks/process-document/trigger`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('TRIGGER_DEV_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
payload: { documentId: parsed.data.documentId },
}),
}
)
// Responder imediatamente, o job roda em background
return new Response(
JSON.stringify({ queued: true }),
{ status: 202, headers: { 'Content-Type': 'application/json' } }
)
})
Condição verificável
# Procurar patterns que indicam processos longos
grep -rn "await.*fetch.*openai\|pdf\|sharp\|ffmpeg\|sleep\|setTimeout" \
volumes/functions/ --include="*.ts" | grep -v "trigger"
# Esperado: nenhum resultado (exceto chamadas curtas de API)
# Existem timeouts no código?
grep -rn "AbortSignal.timeout\|setTimeout" \
volumes/functions/ --include="*.ts"
# Cada fetch externo deveria ter um timeout
Cenário de falha
Uma Edge Function que espera 2 minutos por uma chamada de API de IA bloqueia um slot de worker no container do Edge Runtime. Com múltiplos requests simultâneos, os workers ficam lotados e chamadas de webhook subsequentes (por exemplo do Stripe) falham com timeout. O Stripe interpreta isso como erro e faz retry, o que piora a situação.
B6 - Error handling e responses seguras
Implementação
Edge Functions não podem vazar detalhes internos para o chamador em caso de erro.
// Pattern para error handling seguro
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req)
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// ... lógica principal ...
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
// Log interno (detalhes)
console.error(`Function error: ${error.message}`, {
stack: error.stack,
// NÃO logar secrets ou dados de usuário
})
// Resposta externa (genérica)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
O que NÃO pertence a responses ou logs:
// ERRADO: enviar stack trace ao cliente
return new Response(JSON.stringify({ error: error.stack }), { status: 500 })
// ERRADO: logar secrets
console.log(`Connecting with key: ${Deno.env.get('SERVICE_ROLE_KEY')}`)
// ERRADO: logar dados completos do usuário
console.log(`Processing user: ${JSON.stringify(user)}`)
Condição verificável
# Detalhes de erro são enviados ao cliente?
grep -rn "error\.stack\|error\.message" volumes/functions/ --include="*.ts" | \
grep "Response"
# Esperado: nenhum resultado (stack/message apenas em console.error)
# Secrets são logados?
grep -rn "console\.log.*KEY\|console\.log.*SECRET\|console\.log.*token" \
volumes/functions/ --include="*.ts"
# Esperado: nenhum resultado
Cenário de falha
Quando um stack trace de erro é enviado ao cliente, um invasor vê caminhos internos, nomes de módulos e detalhes de conexão com o banco de dados. Isso facilita consideravelmente ataques direcionados. Quando secrets aparecem em logs, ficam visíveis para qualquer pessoa com acesso ao monitoramento.
Parte C - Operação e monitoramento
C1 - Workflow de deployment
Implementação
No setup Self-Hosted não existe um comando supabase functions deploy. O workflow é baseado em arquivos:
#!/bin/bash
# scripts/deploy-functions.sh
set -euo pipefail
FUNCTIONS_DIR="/opt/supabase/volumes/functions"
SOURCE_DIR="./supabase/functions"
echo "Deploying Edge Functions..."
# 1. Verificação de sintaxe (Deno)
for file in $(find "$SOURCE_DIR" -name "*.ts" -not -path "*/_shared/*"); do
deno check "$file" 2>/dev/null || {
echo "ERRO: erro de sintaxe em $file"
exit 1
}
done
# 2. Copiar Functions
rsync -av --delete \
--exclude='*.test.ts' \
"$SOURCE_DIR/" "$FUNCTIONS_DIR/"
# 3. Reiniciar container
docker compose restart functions --no-deps
# 4. Health Check
sleep 5
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:8000/functions/v1/hello 2>/dev/null || echo "000")
if [ "$HEALTH" = "200" ] || [ "$HEALTH" = "401" ]; then
echo "Deployment de Edge Functions bem-sucedido"
else
echo "AVISO: Health Check Status $HEALTH"
fi
Condição verificável
# Container do Edge Runtime está rodando?
docker compose ps functions --format '{{.State}}'
# Esperado: running
# Functions estão acessíveis?
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${ANON_KEY}" \
http://localhost:8000/functions/v1/stripe-webhook
# Esperado: 200 ou 405 (Method Not Allowed, pois é GET em vez de POST)
C2 - Integração com Claude Code
O Claude Code verifica Edge Functions contextualmente como complemento aos checks determinísticos.
Arquitetura
Git Push / PR
|
+-- Checks determinísticos (CI/CD)
| +-- grep por secrets hardcoded
| +-- grep por verificação de assinatura ausente
| +-- grep por validação de entrada ausente
| +-- grep por detalhes de erro em responses
| +-- Deno Type Check
|
+-- Análise Claude Code (semanal ou em PR)
+-- Novas Functions sem verificação de assinatura?
+-- Uso de service_role adequado?
+-- Patterns de processos longos detectados?
+-- Desvio de arquitetura (lógica de negócio em Functions)?
+-- CORS correto para novas Functions?
Script CI concreto
#!/bin/bash
# scripts/check-edge-functions.sh
REPORT=""
FUNCTIONS_DIR="volumes/functions"
# 1. Secrets hardcoded
SECRETS=$(grep -rn "sk_live\|sk_test\|whsec_\|Bearer ey" \
"$FUNCTIONS_DIR" --include="*.ts" 2>/dev/null)
if [ -n "$SECRETS" ]; then
REPORT+="CRITICO: Secrets hardcoded encontrados:\n$SECRETS\n\n"
fi
# 2. Webhook Functions sem verificação de assinatura
for dir in "$FUNCTIONS_DIR"/*-webhook/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
if ! grep -qE "signature|verify|hmac|crypto" "$dir/index.ts" 2>/dev/null; then
REPORT+="CRITICO: $name não possui verificação de assinatura\n"
fi
done
# 3. Functions sem validação de entrada
for dir in "$FUNCTIONS_DIR"/*/; do
name=$(basename "$dir")
[[ "$name" == "main" || "$name" == "_shared" ]] && continue
if grep -q "JSON.parse" "$dir/index.ts" 2>/dev/null && \
! grep -qE "zod|safeParse|z\." "$dir/index.ts" 2>/dev/null; then
REPORT+="AVISO: $name faz parse de JSON sem Schema Validation\n"
fi
done
# 4. Detalhes de erro em responses
LEAKS=$(grep -rn "error\.stack\|error\.message" "$FUNCTIONS_DIR" --include="*.ts" | \
grep "Response" 2>/dev/null)
if [ -n "$LEAKS" ]; then
REPORT+="AVISO: Detalhes de erro em responses:\n$LEAKS\n\n"
fi
# 5. CORS Wildcards
WILDCARDS=$(grep -rn "'\\*'" "$FUNCTIONS_DIR" --include="*.ts" | grep -i "origin" 2>/dev/null)
if [ -n "$WILDCARDS" ]; then
REPORT+="AVISO: CORS Wildcard encontrado:\n$WILDCARDS\n\n"
fi
# 6. Patterns de processos longos
LONG=$(grep -rn "openai\|sharp\|ffmpeg\|puppeteer" "$FUNCTIONS_DIR" --include="*.ts" 2>/dev/null)
if [ -n "$LONG" ]; then
REPORT+="AVISO: Possíveis patterns de processos longos:\n$LONG\n\n"
fi
# Saída
if [ -n "$REPORT" ]; then
echo -e "=== Edge Function Security Check ===\n"
echo -e "$REPORT"
else
echo "Todos os checks de Edge Function passaram."
fi
O Claude não executa alterações automáticas em produção.
Checklist de deployment
Verificar antes de cada deployment de Edge Functions:
Arquitetura
[ ] Function é uma integração (Webhook/Evento), não lógica de negócio
[ ] Um provedor de webhook por Function
[ ] Processos longos delegados ao Trigger.dev
Segurança
[ ] Assinatura de webhook é verificada
[ ] Entrada é validada com schema (ex: Zod)
[ ] Escolha correta de client (anon vs. service_role)
[ ] service_role apenas quando não há contexto de usuário
Secrets
[ ] Nenhum secret hardcoded no código
[ ] Secrets em .env.functions (não no Git)
[ ] .env.functions com permissões 600
CORS
[ ] Handler OPTIONS presente
[ ] Nenhum origin wildcard em produção
[ ] Headers CORS em TODAS as responses (inclusive de erro)
Error Handling
[ ] Try/Catch em torno de toda a lógica
[ ] Mensagens de erro genéricas para o cliente
[ ] Detalhes apenas em console.error (sem secrets)
Deployment
[ ] Deno Type Check passou
[ ] Container reiniciado após deployment
[ ] Health Check bem-sucedido após deployment
Conclusão
Supabase Edge Functions são um ponto de integração poderoso quando utilizadas como tal: receber webhooks, verificar assinaturas, processar eventos e encaminhá-los.
O ponto decisivo é a delimitação. Edge Functions não são um segundo backend ao lado do Next.js e nem um job runner ao lado do Trigger.dev. Quem traça esse limite com clareza e implementa os fundamentos de segurança (assinaturas, validação de entrada, client correto, CORS), pode operar Edge Functions com segurança no setup Self-Hosted.
A combinação de checks determinísticos no CI e análise contextual do Claude Code cobre tanto patterns conhecidos quanto riscos novos e inesperados. Quem aposta desde o início em Cert-Ready-by-Design economiza rodadas de auditoria posteriores.
Download da checklist de auditoria
Prompt preparado para o Claude Code. Faça upload do arquivo no seu servidor e inicie o Claude Code no diretório do projeto das suas Edge Functions. O Claude Code verificará automaticamente todos os pontos de segurança deste runbook e reportará APROVADO, AVISO ou CRÍTICO.
claude -p "$(cat claude-check-artikel-3-edge-functions-br.md)" --allowedTools Read,Grep,Glob,Bash
Baixar checklistSumário da série
Este artigo faz parte da nossa série DevOps para stacks de aplicações self-hosted.
- Supabase Self-Hosting Runbook
- Next.js sobre Supabase com segurança
- Supabase Edge Functions com segurança - este artigo
- Trigger.dev Background Jobs com segurança
- Claude Code como controle de segurança no workflow DevOps
- Security Baseline para todo o stack
No próximo artigo, mostramos como operar Trigger.dev Background Jobs com segurança, sem criar novos riscos de segurança no stack.

Bert Gogolin
Diretor Executivo, Gosign
AI Governance Briefing
IA empresarial, regulamentação e infraestrutura - uma vez por mês, diretamente de mim.