Pular para o conteúdo
Infraestrutura & Tecnologia

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.

Mansoor Ahmed
Mansoor Ahmed
Head of Engineering 16 min de leitura

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-runtime e 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.

  1. Supabase Self-Hosting Runbook
  2. Next.js sobre Supabase com segurança
  3. Supabase Edge Functions com segurança - este artigo
  4. Trigger.dev Background Jobs com segurança
  5. Claude Code como controle de segurança no workflow DevOps
  6. 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érioEdge FunctionServer ActionTrigger.dev Task
OrigemEvento externo (webhook)Entrada do usuárioTarefa interna
Tempo de execução< 30 segundos< 10 segundosAté 5 minutos+
AutenticaçãoAssinatura HMACSessão do usuário (JWT)Chave API / service_role
RLSIgnorado (service_role)Ativo (anon key)Ignorado (service_role)
RetryProvedor do webhookNenhum (síncrono)Integrado (Trigger.dev)
ExemploStripe checkout webhookFormulário de perfilGeraçã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 checklist

Sumário da série

Este artigo faz parte da nossa série DevOps para stacks de aplicações self-hosted.

  1. Supabase Self-Hosting Runbook
  2. Next.js sobre Supabase com segurança
  3. Supabase Edge Functions com segurança - este artigo
  4. Trigger.dev Background Jobs com segurança
  5. Claude Code como controle de segurança no workflow DevOps
  6. 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

Bert Gogolin

Diretor Executivo, Gosign

AI Governance Briefing

IA empresarial, regulamentação e infraestrutura - uma vez por mês, diretamente de mim.

Sem spam. Cancelável a qualquer momento. Política de privacidade

Supabase Edge Functions Deno Webhooks Security
Compartilhar este artigo

Perguntas frequentes

Para que servem as Supabase Edge Functions?

Edge Functions são adequadas para webhooks, integrações com APIs externas e endpoints de eventos assinados. Elas não são uma alternativa a Next.js Server Actions para lógica de negócio e nem um substituto para o Trigger.dev em processos de longa duração.

Por que todo endpoint de webhook precisa verificar a assinatura?

Sem verificação de assinatura, qualquer pessoa pode enviar payloads arbitrários para o webhook. Um invasor poderia disparar eventos falsificados, por exemplo uma confirmação de checkout falsa no Stripe, sem que tenha havido qualquer pagamento.

Por que webhooks precisam de verificação de assinatura mas não de JWT?

Webhooks são chamados por serviços externos como Stripe ou GitHub que não possuem um JWT do Supabase. Em vez disso, eles assinam o payload com um secret compartilhado (HMAC). A Edge Function deve verificar essa assinatura porque caso contrário qualquer pessoa pode enviar payloads arbitrários.