Skip to content

ADR-003 — Edge Function como Orquestrador (action-dispatch)

CampoValor
Status✅ Aceito
Data2026-04-30
AutoresEquipe OLP
Implementação60+ Edge Functions em supabase/functions/

Contexto

A plataforma tem ~60 Edge Functions agrupadas por domínio (gestao-alunos, admin-escolas, coordenador-controle, etc.). Cada domínio tipicamente tem 5–15 operações relacionadas (list, get, create, update, delete, importar, exportar, ações específicas).

Existiam 3 caminhos plausíveis:

  1. REST puro: 1 endpoint por operação (GET /alunos, POST /alunos/:id/transferir).
  2. GraphQL / RPC tipado: schema único com resolvers.
  3. Action-dispatch: 1 Edge Function por domínio, body com { action: 'create', ... }.

Decisão

Toda Edge Function de domínio segue o padrão action-dispatch:

ts
// supabase/functions/gestao-alunos/index.ts
Deno.serve(async (req) => {
  const { action, ...payload } = await req.json();
  const user = await extractAuthenticatedUser(req);

  switch (action) {
    case 'list':       return list(user, payload);
    case 'create':     return create(user, payload);
    case 'transferir': return transferir(user, payload);
    default: return jsonError(400, 'Action desconhecida');
  }
});

Contrato de resposta (invariante)

ts
// Sucesso
{ success: true, data: T }
// Erro
{ success: false, message: string, code?: string }

Regras

  1. 1 Edge Function = 1 domínio de negócio (não 1 operação).
  2. Toda action valida payload com Zod na entrada (memória zod-validation-standard).
  3. extractAuthenticatedUser(req) é a primeira linha — fail-close.
  4. CORS em todas as respostas, incluindo no catch final.
  5. Status HTTP semântico: 401 (sem auth), 403 (auth mas sem permissão / 0 rows affected), 400 (payload), 404, 409 (conflito), 500 (erro técnico). Nunca 500 para auth.
  6. Frontend chama via invokeAction(funcao, action, params) (ADR-004) — nunca fetch direto.

Alternativas consideradas

A. REST puro (1 endpoint por operação)

Pros: cacheável por path, padrão da indústria. Cons: cada operação exige nova entrada em config.toml + deploy; routing dentro do Deno é manual; cold-start por endpoint vira problema com 200+ operações. Veredicto: rejeitado — overhead operacional alto para Deno Edge.

B. GraphQL / RPC tipado (tRPC-like)

Pros: tipagem ponta-a-ponta. Cons: setup complexo em Deno; debugging de queries N+1; difícil casar com RLS row-level; curva alta para devs juniores. Veredicto: rejeitado — ganho de tipo não compensa quando Zod + EdgeResponse<T> já cobrem 90%.

C. PostgREST direto (sem Edge Function)

Pros: zero código backend. Cons: lógica de negócio (validação cruzada, side-effects, integração externa) impossível; logging fire-and-forget impossível; logs de auditoria precisariam de triggers complexos. Veredicto: rejeitado para qualquer operação com lógica — usado apenas em poucos casos read-only.

Consequências

Positivas

  • 1 deploy = 1 domínio inteiro atualizado
  • Compartilhar helpers (_shared/) sem duplicação
  • config.toml com 60 entradas em vez de 600
  • Permissão por ação centralizada no switch (fácil de auditar)
  • Frontend tem 1 hook React Query por domínio com mutationFn que troca o action

Negativas

  • Edge Function pode crescer (>400 linhas → splitting por arquivo de action, regra code-splitting-policy)
  • Observabilidade: logs precisam carregar action como dimensão (já fazem via LOG_ACTION_OPTIONS)
  • Não-cacheável por HTTP (todas as requests são POST) — mitigado por React Query no cliente

Trade-offs

EixoCustoBenefício
Granularidade HTTPTudo POST, não cacheável por CDNCache real fica no React Query (mais relevante)
Tamanho do arquivoPode passar 400 linhasSplitting por action (helpers em _shared/)
Padrão fora do convencionalOnboarding precisa explicarPadrão consistente em 60 funções

Conformidade

Como verificar:

bash
# Toda Edge Function nova deve ter entry em config.toml com verify_jwt = false
# (auth é feita em código)
rg "Deno.serve" supabase/functions --type ts -l | while read f; do
  fname=$(basename $(dirname $f))
  grep -q "\[functions.$fname\]" supabase/config.toml || echo "FALTA config.toml: $fname"
done

Sinais de violação:

  • Edge Function sem extractAuthenticatedUser na primeira linha (exceto webhooks/pre-auth)
  • Resposta sem o invariante { success, ... }
  • Frontend chamando com fetch direto em vez de invokeAction

Débito técnico conhecido

  • Algumas funções (mercadopago-webhook) não seguem action-dispatch porque recebem payload do provedor (formato externo). Aceito — webhooks externos não controlam o body.
  • eventos-calendario-trial é variante do eventos-calendario por necessidade de cliente diferente; consolidação em backlog.

Referências

  • docs/development/NEW_EDGE_FUNCTION.md
  • docs/architecture/EDGE_FUNCTIONS_CATALOG.md
  • mem://architecture/supabase-error-handling-pattern
  • mem://architecture/zod-validation-standard
  • mem://architecture/auth-and-idor-status-code-semantics
  • ADR-004 — consumidor canônico
  • ADR-009 — formato de erro