Estado da Arte — Padrões Obrigatórios (v1.0)
Documento-guia consolidado com todos os padrões mandatórios para novas features e manutenção. Atualizado: 2026-03-26
1. Backend (Edge Functions)
1.1 Template Obrigatório
Toda Edge Function deve seguir o template:
import { handleCorsPrelight, getCorsHeaders } from '../_shared/cors-helpers.ts';
import { extractAuthenticatedUser } from '../_shared/auth-helpers.ts';
import { createSupabaseClient } from '../_shared/supabase-client.ts';
import { registrarLog } from '../_shared/logging-helper.ts';
Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') return handleCorsPrelight(req);
const corsHeaders = getCorsHeaders(req);
try {
const { action, ...payload } = await req.json();
// Ações públicas ANTES do auth check
if (action === 'public_action') { /* ... */ }
// Auth check
const user = await extractAuthenticatedUser(req);
if (!user) return new Response(JSON.stringify({ success: false, message: 'Não autenticado' }), { status: 200, headers: corsHeaders });
const supabase = createSupabaseClient(req);
// Roteamento por ação
if (action === 'list') { /* ... */ }
return new Response(JSON.stringify({ success: false, message: 'Ação não reconhecida' }), { status: 200, headers: corsHeaders });
} catch (error) {
console.error('[nome-funcao] Erro:', error);
return new Response(JSON.stringify({ success: false, message: 'Erro interno. Tente novamente.' }), { status: 500, headers: corsHeaders });
}
});1.2 Nomenclatura de Clientes
| Variável | Factory | Quando usar | RLS |
|---|---|---|---|
supabase | createSupabaseClient(req) | 99% — operações autenticadas | Ativo |
supabasePublic | createSupabasePublic() | Lookup público | Ativo |
supabaseSystem | createSupabaseSystem() | Logging, auth, crons | Bypass |
PROIBIDO: supabaseAdmin, createClient inline, service_role no frontend.
1.3 Sanitização de Erros
- NUNCA retornar
error.messagepara o cliente - Usar mensagens contextuais por operação:
"Não foi possível carregar os dados." console.errorpara debug interno; mensagem genérica na Response- Catch global sempre retorna:
"Erro interno. Tente novamente."
1.4 Padrão de Split
| Linhas | Ação |
|---|---|
| < 800 | Arquivo único OK |
| 800–1500 | Avaliar split |
| > 1500 | Split obrigatório |
Padrão: index.ts como orquestrador fino (~150-350 linhas) + helpers em _shared/.
Regras do Split:
- Orquestrador mantém: roteamento (
switch/if), estado in-memory (ex:burstTracker),EdgeRuntime.waitUntil, auth guard - Helpers exportam funções puras que recebem dependências como parâmetro (injeção de dependência)
- Cada helper retorna
Responsediretamente — o orquestrador delega sem manipular o retorno - Helpers NÃO importam entre si (evitar grafo de dependência circular)
- Constantes compartilhadas ficam em um helper dedicado (ex:
portal-security.ts)
Splits realizados (Mar/2026):
| Edge Function | Antes | Depois (orq.) | Helpers |
|---|---|---|---|
portal-escola | 3.929 linhas | 212 linhas | 6: portal-security, portal-login-aluno, portal-login-responsavel, portal-cadastro, portal-dashboard, portal-config |
gestao-resultados | 3.137 linhas | ~190 linhas | 5: resultados-compute, resultados-queries, resultados-mutations, resultados-config, resultados-import |
gestao-alunos | 2.413 linhas | ~170 linhas | 4: alunos-crud, alunos-validation, alunos-transfer, alunos-import |
Catálogo completo dos helpers:
docs/architecture/EDGE_FUNCTIONS_CATALOG.md→ seção "Shared Helpers".
1.5 Logging
registrarLog()em todo CREATE / UPDATE / DELETEreqobrigatório (geolocalização via Cloudflare headers)- Ação:
<modulo>.<operacao>lowercase - Diff:
gerarAlteracoes(antes, depois)de_shared/diff-helper.ts
1.6 RLS Silent-Failure Check
Mutations com RLS ativo devem validar o resultado:
const { data, error } = await supabase.from('tabela').update({...}).eq('id', id).select().maybeSingle();
if (!data && !error) {
return new Response(JSON.stringify({ success: false, message: 'Sem permissão.' }), { status: 403, headers: corsHeaders });
}1.7 Batching de Requisições (batch_init)
Telas com 3+ requests ao mesmo backend DEVEM implementar action batch_init:
- Backend usa
Promise.allpara paralelizar queries internas - Elimina overhead repetido de auth/feature-flag/CORS (~100-300ms por request)
- Resposta única com todos os dados:
{ olimpiadas, stats, config }
Exemplos implementados:
| Tela | Action | Requests consolidados |
|---|---|---|
| Resultados | batch_init_resultados | 4 → 1 |
| Mural | batch_init | 4 → 1 |
1.8 Resiliência a Erros Transitórios
invokeEdgefaz retry automático (max 2) com backoff para 502/503/504queryFnDEVE fazerthrowem erro — nunca retornar estado vazio ([])- Retornar
[]em erro impede retry do React Query e mascara falhas - Auth context diferencia timeout transitório (
_transient) de sessão expirada
// ❌ PROIBIDO — engole erro, impede retry
queryFn: async () => {
try { return await fetchData(); }
catch { return []; } // React Query acha que deu certo
}
// ✅ CORRETO — throw propaga para retry
queryFn: async () => {
const result = await fetchData();
if (!result.success) throw new Error(result.message);
return result.data;
}2. Frontend (Hooks)
2.1 React Query — Obrigatório
Critérios: listagem, dados compartilhados, paginação/filtros, CRUD, dados que crescem.
Exceções válidas (NÃO migrar):
- Portal público (
useMuralPortal) — sessão cookie-based - WebSocket realtime (
useRealtimeNotificacoes) - Import stateful (
useImportacaoSessao,useImportacaoResultados)
2.2 Template de Hook
const MODULO_KEYS = {
all: ['modulo'] as const,
list: (filtros: Filtros) => ['modulo', filtros] as const,
};
const CACHE_CONFIG = {
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
refetchOnWindowFocus: false,
};2.3 Tratamento de Erros
getUserFriendlyError(error)em todoonErrorde mutationsolpToast.error()para feedback visual- PROIBIDO:
alert(),error.messagedireto no toast, importsonnerdireto
2.4 staleTime por Categoria
| Categoria | staleTime | Exemplo |
|---|---|---|
| Quase estático | 30min | Séries, anos |
| Configurações | 10min | Permissões |
| Operacional | 5min | Alunos, turmas |
| Dashboards | 5min | Métricas |
| Tempo real | 1min | Notificações |
2.5 Estabilidade de Hooks (useCallback) — 🔴 OBRIGATÓRIO
Toda função retornada por hook custom DEVE usar useCallback.
Funções sem useCallback criam nova referência a cada render. Quando usadas como dependências de useEffect, causam loop infinito de re-renders que congela a UI completamente.
Regras
- Dependências do
useCallbackdevem ser estáveis:queryClient,mutation,refetch— nunca dados reativos comoitems.length useEffectde mount redundante é proibido quando React Query já auto-carrega viauseQuery- Se um
useEffectchama uma função que é essencialmente um refetch → remover ouseEffect
Exemplo
// ❌ PROIBIDO — cria nova ref a cada render → loop infinito
const useMeuHook = () => {
const listarDados = async () => { /* ... */ };
return { listarDados }; // ref instável!
};
// ✅ CORRETO — ref estável entre renders
const useMeuHook = () => {
const listarDados = useCallback(async () => { /* ... */ }, [queryClient]);
return { listarDados }; // ref estável ✓
};2.6 enabled — Sem Dependência Circular
enabled de um useQuery NUNCA deve depender de dado que só existe no resultado da própria query.
// ❌ DEADLOCK — anoEdicao só é setado quando query retorna, mas query não roda sem ele
useQuery({ enabled: anoEdicao !== null, queryFn: () => fetchDados(anoEdicao) });
// ✅ CORRETO — primeira chamada sem filtro, refinamento posterior
useQuery({ enabled: true, queryFn: () => fetchDados() });
// Depois, com queryKey diferente para filtro específico3. Frontend (Componentes)
3.1 Feature Directory Pattern
Componentes > 400 linhas ou 3+ subcomponentes:
src/components/dominio/
index.tsx ← orquestrador (hooks, export único)
helpers.ts ← tipos + funções puras (sem React)
dominio-*.tsx ← subcomponentes com prefixo3.2 Regras
- Prefixo de domínio em subcomponentes
- Hooks de dados no orquestrador (props estritamente tipadas para sub)
- Separação helpers.ts para lógica pura testável
4. Segurança
4.1 Princípios
- Zero Trust: Backend revalida tudo (nunca confiar no frontend)
- Fail-Close: Erro = bloqueia (não permite acesso)
- Sem SERVICE_ROLE no frontend
- Sem createClient inline em Edge Functions
- Rate limits em endpoints públicos (portal)
4.2 Autenticação
- Cookie HttpOnly (
olp_auth8h,olp_mural2h) - JWT customizado via
extractAuthenticatedUser(req) verify_jwt = falseno config.toml (auth via cookies)
5. Testes
5.1 Três Camadas
| Camada | Ferramenta | O que testa |
|---|---|---|
| Unidade | Vitest | Lógica pura, visibilidade |
| Integração | Vitest + QueryClient real | Ciclo React Query, invalidação |
| Contrato | Deno tests | CORS, auth guard, response format |
| E2E | Playwright | Jornadas cross-role |
5.2 Padrão de Integração
integration-helpers.tscom QueryClient real- Mock apenas do
invokeAction/invokeEdge(HTTP boundary) - Spy em
invalidateQueriespara validar cache wiring vi.mockfactory inline (não usar variáveis externas — hoisting)
6. Documentação
6.1 Regra Zero
Toda mudança significativa DEVE atualizar docs/ na mesma entrega.
6.2 Hierarquia
- Knowledge (project settings): Sucinto, regras críticas, referências para
docs/ docs/: Detalhado, com exemplos e templates@audit: Checklist de validação contínua (16 seções, 20+ domínios)
1.9 Validação de Input com Zod (Roadmap)
Status: Planejado — implementação faseada.
Atualmente nenhuma Edge Function valida input com schema formal. O padrão futuro usará Zod via import Deno:
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts';
const schema = z.object({
nome: z.string().min(1).max(255),
escola_id: z.string().uuid(),
});
const parsed = schema.safeParse(params);
if (!parsed.success) {
return new Response(JSON.stringify({
success: false,
message: 'Dados inválidos',
}), { status: 400, headers: corsHeaders });
}Fases de implementação:
| Fase | Escopo | EFs |
|---|---|---|
| 1 | Criar _shared/validation.ts com helper validateParams() | — |
| 2 | Auth, pagamentos, portal | ~8 |
| 3 | Demais EFs (gradual, por manutenção) | ~45 |
7. Hooks Migrados para React Query (Referência)
Todos os hooks abaixo foram migrados e estão em conformidade:
useAdminEscolas, useBannersLogin, useEventosCalendario, useTarefasEscola, useGestaoResultados, useImportacaoResultados, useMuralEscola, useHeadersNovidades, useCursosData, useTutoriaisData, useTemplatesData, useOlimpiadasData, useOlimpiadasCoordenador, useInscricoesOlimpiada, useVideosCoord, useComunicacaoEscola, useAdminDashboardMetrics, useAdminUsuariosEscola, useEscolaDados, useAnoLetivo, useTransferenciaAlunos, useEscolaPagamentos, useMuralConfig, useGestaoTurmas, useGestaoAlunos, useGestaoResponsaveis, useAdminLogs, useAdminUsuarios, useUserProfile, useTrialInfo, useNotificacoes, useAdminSmsLogs, useAdminAssinaturas, useAdminFaturas, useMercadoPago, useUsuariosEscola, useAdminCronMonitor
Referências
- REACT_QUERY_CACHE.md — Cache patterns detalhados
- CODING_STANDARDS.md — Convenções de código
- NEW_EDGE_FUNCTION.md — Template de Edge Functions
- AUDIT_CHECKLIST.md — Checklist @audit
- PROBLEM_SOLVING.md — Protocolo de resolução