← Fase 1 ET-LIC-2026-001 Fase 2/3
⚙ ET-LIC-2026-001 · Fase 2 de 3

APIs, Integrações,
Segurança e Infraestrutura

Módulo de Licenciamento · Sistema Pipeline · Vendemmia
Documento
ET-LIC-2026-001
Fase
2 de 3
Seções
4 · 5 · 6
API Base
Supabase PostgREST
Data
Maio 2026

4. APIs e Integrações Seção 4

O sistema utiliza exclusivamente a API REST gerada automaticamente pelo PostgREST do Supabase, sem APIs customizadas próprias. Toda a comunicação segue o padrão HTTPS + JSON com autenticação via JWT Bearer token no header Authorization.

PostgREST — API automática: O Supabase expõe automaticamente uma REST API completa sobre cada tabela PostgreSQL. Os endpoints seguem o padrão /rest/v1/{tabela} com suporte a filtros (eq, like, is...), seleção de colunas (select), ordenação (order), paginação (limit/offset) e operações CRUD completas via métodos HTTP.

4.1 PostgREST — Endpoints do Módulo Licenciamento

Base URL

Base URL:  https://geojgcncmtbjebftosor.supabase.co/rest/v1

Headers obrigatórios:
  apikey:        {SUPABASE_ANON_KEY}
  Authorization: Bearer {JWT_TOKEN}
  Content-Type:  application/json
  Prefer:        return=representation  // para receber o registro inserido/atualizado
GET/lic_leadsListar todos os leads
Query params mais usados
// Buscar todos os leads ordenados por data
GET /lic_leads?order=created_at.desc

// Filtrar só convertidos
GET /lic_leads?status=eq.Convertido

// Buscar por token do portal
GET /lic_leads?form_token=eq.TOKEN&limit=1

// Selecionar campos específicos
GET /lic_leads?select=id,razao_social,status,uf,area_total_m2
Response
[
  {
    "id": 1,
    "razao_social": "Logística ABC Ltda.",
    "status": "Convertido",
    "uf": "SP",
    "area_total_m2": 12000,
    "created_at": "2026-03-15T10:30:00Z"
  }
]
Códigos de retorno
  • 200 Array de registros (vazio se nenhum resultado)
  • 401 JWT inválido ou ausente
  • 403 RLS bloqueou o acesso
POST/lic_leadsCriar novo lead
Body (JSON)
{
  "razao_social": "Logística ABC Ltda.",
  "status": "Ativo",
  "etapa": "Prospecção",
  "criado_por": "gppereira@vendemmia.com.br",
  "created_at": "2026-05-30T14:00:00Z"
}
Retorno com Prefer: return=representation
// Retorna o registro criado com id gerado
[{ "id": 42, "razao_social": "Logística ABC Ltda.", ... }]
Códigos de retorno
  • 201 Registro criado
  • 409 Conflito (ex: form_token duplicado)
  • 422 Violação de constraint NOT NULL
PATCH/lic_leads?id=eq.{id}Atualizar campos de um lead
Body (JSON — apenas campos a atualizar)
{
  "status": "Convertido",
  "updated_at": "2026-05-30T14:30:00Z"
}
Códigos de retorno
  • 200 Atualizado (com Prefer: return=representation)
  • 204 Atualizado sem body de resposta
  • 404 Registro não encontrado (filtro não casou)
DELETE/lic_leads?id=eq.{id}Excluir lead (cascata nas tabelas relacionadas)

A exclusão é cascata via ON DELETE CASCADE nas FKs de lic_auditoria, lic_performance, lic_historico e lic_anexos. Arquivos no Storage NÃO são removidos automaticamente.

Códigos de retorno
  • 204 Excluído com sucesso
  • 404 Registro não encontrado

Endpoints das demais tabelas do módulo

TabelaMétodos usadosFiltros principaisObservação
lic_auditoriaGET, POST, PATCHlead_id=eq.{id}UNIQUE por lead_id — máx. 1 registro por lead
lic_performanceGET, POSTlead_id=eq.{id}&order=periodo_ref.ascMúltiplos por lead (histórico trimestral)
lic_historicoGET, POSTlead_id=eq.{id}&order=created_at.descAppend-only; sem UPDATE nem DELETE no uso normal
lic_anexosGET, POST, DELETElead_id=eq.{id} ou tipo=like.foto_armazem%DELETE também remove o arquivo do Storage

4.2 Supabase Auth API

EndpointMétodoFunçãoSDK call
/auth/v1/token?grant_type=passwordPOSTLogin com e-mail + senha → retorna JWTsupabaseClient.auth.signInWithPassword()
/auth/v1/logoutPOSTLogout — invalida o refresh tokensupabaseClient.auth.signOut()
/auth/v1/token?grant_type=refresh_tokenPOSTRefresh do JWT (automático pelo SDK)Automático
/auth/v1/recoverPOSTEnvio de e-mail de reset de senhasupabaseClient.auth.resetPasswordForEmail()
/auth/v1/userPUTAtualização de senha (após reset)supabaseClient.auth.updateUser()

Estrutura do JWT Token

// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload (claims relevantes)
{
  "iss": "supabase",
  "ref": "geojgcncmtbjebftosor",
  "role": "authenticated",         // ou "anon" para portal público
  "email": "gppereira@vendemmia.com.br",
  "sub": "uuid-do-usuario",
  "iat": 1748610000,
  "exp": 1748613600              // expira em 1h; SDK faz refresh automático
}

4.3 Supabase Storage API

OperaçãoEndpointMétodoAutenticação
Upload de arquivo/storage/v1/object/lic-anexos/{path}POST (multipart)JWT (authenticated ou anon com policy)
Download público/storage/v1/object/public/lic-anexos/{path}GETNenhuma (bucket público)
Obter URL públicaSDK: getPublicUrl(path)Operação local (sem chamada de rede)
Deletar arquivo/storage/v1/object/lic-anexos (body: paths[])DELETEJWT authenticated
Listar arquivos/storage/v1/object/list/lic-anexosPOST (body: prefix, limit)JWT authenticated

Padrão de upload com tratamento de erros

async function uploadAnexo(file, leadId, tipo) {
  const safe = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
  const path = `leads/${leadId}/${tipo}/${Date.now()}_${safe}`;

  const { error: upErr } = await supabaseClient.storage
    .from('lic-anexos')
    .upload(path, file, {
      cacheControl: '3600',
      upsert: false  // nunca sobrescrever — sempre gerar novo path
    });

  if (upErr) throw upErr;  // 409 = path já existe, 413 = arquivo muito grande

  const { data: urlData } = supabaseClient.storage
    .from('lic-anexos')
    .getPublicUrl(path);  // operação local — URL determinística

  return { path, url: urlData.publicUrl };
}

4.4 APIs Externas

APIEndpointUsoAuthFallback
BrasilAPI v2https://brasilapi.com.br/api/cep/v2/{cep}Geocodificação por CEP (lat/lng + endereço)NenhumaViaCEP
ViaCEPhttps://viacep.com.br/ws/{cep}/json/Endereço por CEP (sem coordenadas)NenhumaMensagem de erro
CartoDB Voyagerhttps://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.pngTiles do mapa interativoNenhumaMapa em branco
GitHub GeoJSONhttps://raw.githubusercontent.com/giuliano-macedo/geodata-br-states/main/geojson/br_states.jsonPolígonos dos estados para choroplethNenhumaSem choropleth
QR Server APIhttps://api.qrserver.com/v1/create-qr-code/?size=280x280&data={url}Geração de QR Code para link do portalNenhumaLink apenas (sem QR)

BrasilAPI — Response da geocodificação por CEP

// GET https://brasilapi.com.br/api/cep/v2/01310100
{
  "cep": "01310-100",
  "state": "SP",
  "city": "São Paulo",
  "neighborhood": "Bela Vista",
  "street": "Avenida Paulista",
  "location": {
    "type": "Point",
    "coordinates": {
      "longitude": "-46.6526056",
      "latitude": "-23.5640265"
    }
  }
}

4.5 Padrão de Tratamento de Erros

// Padrão de chamada com tratamento completo
async function carregarLicLeads() {
  try {
    const { data, error } = await supabaseClient
      .from('lic_leads')
      .select('*')
      .order('created_at', { ascending: false });

    if (error) throw error;   // erro da API → capturado pelo catch

    baseLicLeads = data || [];

  } catch (e) {
    // showToast = função global de notificação
    showToast('Erro ao carregar leads: ' + (e.message || e), 'error');
    baseLicLeads = [];  // estado seguro mesmo com erro
  }
}

// Objeto de erro do Supabase
{
  message: "JWT expired",       // mensagem legível
  code:    "PGRST301",        // código PostgREST
  hint:    "...",             // dica de resolução
  details: "..."              // detalhes técnicos
}
Código HTTPCódigo PostgRESTSignificadoAção no frontend
200SucessoProcessar data[]
201Criado com sucessoToast verde + fechar modal
204Atualizado/Deletado sem bodyToast verde
400PGRST100Sintaxe inválida na queryLog console + toast erro
401PGRST301JWT ausente ou inválidoRedirecionar para login
403PGRST302RLS bloqueouToast "Acesso negado"
404Registro não encontradoToast "Não encontrado"
409PGRST204Conflito de constraint (UNIQUE)Toast com mensagem específica
422PGRST203Violação NOT NULL ou CHECKToast com campo inválido
500+Erro interno SupabaseToast "Erro interno, tente novamente"

5. Segurança Seção 5

5.1 JWT e Gestão de Sessão

🔑Ciclo de vida do JWT
  • JWT emitido pelo Supabase Auth após login bem-sucedido com e-mail + senha
  • Expiração: 3.600 segundos (1 hora) — configurável no Supabase Dashboard
  • Refresh token: Emitido junto ao JWT; válido por 7 dias; armazenado no localStorage pelo SDK
  • Refresh automático: O SDK @supabase/supabase-js detecta JWT próximo da expiração e faz refresh transparente antes de qualquer chamada
  • Armazenamento: localStorage['sb-geojgcncmtbjebftosor-auth-token'] — sobrevive ao reload da página
  • Logout: Remove o item do localStorage E invalida o refresh token no servidor (blacklist)
// Recuperação de sessão existente (no carregamento da página)
const { data } = await supabaseClient.auth.getSession();
if (data?.session) {
  EMAIL_USUARIO_LOGADO = data.session.user.email.toLowerCase();
  await carregarPerfil(data.session.user.email);
  iniciarSistema();
}

5.2 Controle de Acesso ao Módulo Licenciamento

🛡Whitelist de acesso — proteção em dupla camada
// Camada 1: Botão oculto no HTML
const LICENCIAMENTO_WHITELIST = [
  'gppereira@vendemmia.com.br',
  'jprado@vendemmia.com.br'
];

// Em aplicarPermissoes() — chamada após login
mostrar('nav-licenciamento', LICENCIAMENTO_WHITELIST
  .includes(EMAIL_USUARIO_LOGADO));

// Camada 2: Guard na função navegar()
function navegar(pagina) {
  if (pagina === 'licenciamento' &&
      !LICENCIAMENTO_WHITELIST.includes(EMAIL_USUARIO_LOGADO)) {
    return;  // bloqueia mesmo se URL for manipulada
  }
  // ...
}
Limitação e evolução
  • A whitelist é hardcoded no JavaScript do cliente — visível via DevTools
  • A proteção real é fornecida pelo RLS do PostgreSQL — sem JWT válido, nenhum dado é retornado
  • Evolução recomendada: Implementar tabela lic_permissoes no banco com perfis de acesso, eliminando o hardcode

5.3 Row Level Security — Arquitetura de Políticas

TabelaPolítica atualRiscoEvolução recomendada
lic_leadsALL USING (true) — permissivaMédioRestringir por auth.email() = ANY(whitelist)
lic_auditoriaALL USING (true) — permissivaMédioIdem
lic_performanceALL USING (true) — permissivaMédioIdem
lic_historicoALL USING (true) — permissivaMédioSELECT para todos autenticados; INSERT apenas para autores
lic_anexosALL USING (true) — permissivaMédioINSERT anon permitido; DELETE apenas para authenticated

Implementação futura de RLS granular

-- Política avançada para lic_leads (versão futura)
CREATE POLICY "lic_leads_whitelist" ON lic_leads
  FOR ALL
  TO authenticated
  USING (
    auth.email() IN (
      SELECT email FROM lic_permissoes WHERE ativo = true
    )
  )
  WITH CHECK (
    auth.email() IN (
      SELECT email FROM lic_permissoes WHERE ativo = true
    )
  );

-- Política específica para portal público (anon com token válido)
CREATE POLICY "lic_leads_portal_anon" ON lic_leads
  FOR SELECT
  TO anon
  USING (form_token IS NOT NULL);

CREATE POLICY "lic_leads_portal_update" ON lic_leads
  FOR UPDATE
  TO anon
  USING (form_token IS NOT NULL)
  WITH CHECK (form_token IS NOT NULL);

5.4 Segurança de Storage e Uploads

📦Controles de segurança nos uploads
  • Validação de tipo no frontend: accept nos inputs file + validação JS antes do upload
  • Validação de tamanho: Máximo 10 MB no frontend (20 MB para relatório de visita); arquivo maior é rejeitado com alerta
  • Path isolation: Todos os arquivos sob leads/{lead_id}/ — prevenção de path traversal via sanitização do nome
  • Sanitização do nome: file.name.replace(/[^a-zA-Z0-9._-]/g, '_') — remove chars especiais
  • Timestamp no path: Date.now() prefixado previne sobrescrita acidental e conflitos de nome
  • upsert: false: Upload com upsert: false retorna 409 se o path já existir — nunca sobrescreve arquivos existentes
Tipos de arquivo aceitos por categoria
Tipo de uploadExtensões aceitasTamanho máx.
Contrato Social.pdf, .jpg, .jpeg, .png, .doc, .docx10 MB
Fotos de armazéns.jpg, .jpeg, .png, .heic, .webp10 MB cada
Certificados.pdf, .jpg, .jpeg, .png10 MB cada
Relatório de visita técnica.pdf, .doc, .docx20 MB

Ausência de validação server-side de tipo: A validação de tipo de arquivo é feita apenas no frontend. O Supabase Storage não valida extensões ou MIME types por padrão. Evolução: implementar Supabase Edge Function para validar MIME type no servidor antes de aceitar o upload.

5.5 LGPD e Privacidade de Dados

Categoria de dadoExemplosBase legal (LGPD)Medidas implementadas
Dados pessoaisNome do contato, e-mail, telefone, cargoLegítimo interesse (prospecção B2B)HTTPS em trânsito; RLS no banco; acesso restrito via whitelist
Dados empresariaisCNPJ, razão social, faturamento, armazénsExecução de contrato / legítimo interesseMesmas medidas; tratados como confidenciais
DocumentosContrato Social, certificados, fotosExecução de contratoSupabase Storage; URLs públicas mas sem indexação; acesso apenas por URL direta
Dados de comportamentoHistórico de eventos, KPIsLegítimo interesse (gestão operacional)Armazenados no banco com RLS; sem analytics externo
CredenciaisE-mail e senha dos usuários internosGerenciado pelo Supabase Auth; senha nunca armazenada em plaintext; hash bcrypt

Direitos dos titulares: O sistema não possui interface para que parceiros (leads) solicitem exclusão ou acesso aos seus dados diretamente. Estas solicitações devem ser atendidas manualmente pela equipe Vendemmia via painel Supabase. Recomenda-se implementar endpoint de autoexclusão no portal público em versão futura.

5.6 Análise de Vulnerabilidades

VulnerabilidadeStatusMitigação atualRisco residual
XSS (Cross-Site Scripting)MitigadoTemplate literals com interpolação direta de dados do banco — sem innerHTML com dados não sanitizados em campos críticosBaixo — dados do banco são inseridos em contexto HTML conhecido
SQL InjectionMitigadoToda comunicação via SDK Supabase com queries parametrizadas; PostgREST nunca executa SQL dinâmicoNenhum
CSRFMitigadoSPA com JWT Bearer token no header (não cookie) — CSRF não aplicável para Bearer authNenhum
Exposição da anon keyAceitávelA anon key é projetada para ser pública — acesso real controlado pelo RLS e pelo JWT. Não é a service_role keyBaixo — documentado pelo Supabase como seguro para cliente
Path traversal em StorageMitigadoSanitização do nome do arquivo + path prefixado por lead_idBaixo
RLS permissivoDébito técnicoPolítica permissiva atual; whitelist no frontend como controle principalMédio — endereçar na evolução com RLS por e-mail
localStorage (JWT)AceitávelPadrão do Supabase SDK; alternativa (httpOnly cookie) requer backend próprioBaixo no contexto B2B interno
Upload de malwareParcialExtensões restritivas no frontend; Storage não escaneia vírusMédio — evoluir com ClamAV via Edge Function

5.7 Logs e Auditoria

Fonte de logOnde acessarRetençãoO que captura
Supabase API logsDashboard → Logs → API7 dias (Pro)Todas as chamadas REST: endpoint, método, status, latência, IP
Supabase Auth logsDashboard → Logs → Auth7 diasLogins, logouts, resets de senha, falhas de autenticação
Supabase Postgres logsDashboard → Logs → Postgres7 diasQueries SQL lentas (>1s), erros de constraint, deadlocks
lic_historico (app-level)Tabela no bancoPermanenteEventos de negócio: mudanças de etapa, KPIs, certificados, reuniões
GitHub Actionsgithub.com/Gueilee/pipeline-comercial90 diasDeploys: commit, autor, timestamp, status

6. Infraestrutura Seção 6

6.1 Hosting e CDN

🌐
GitHub Pages
pipelinevendemmia.com.br
  • Hospedagem de arquivos estáticos (HTML, CSS, JS, imagens)
  • CDN Akamai com PoPs globais — baixa latência no Brasil
  • HTTPS automático via Let's Encrypt
  • Domínio customizado configurado via CNAME
  • SLA implícito: 99,9% (Akamai CDN)
  • Zero custo operacional
  • Sem server-side rendering — apenas arquivos estáticos
🗄
Supabase Cloud
sa-east-1 (São Paulo)
  • PostgreSQL 17.6.1 gerenciado (sem manutenção operacional)
  • PostgREST auto-configurado sobre todas as tabelas
  • Auth, Storage e Realtime incluídos no plano
  • Região sa-east-1 — latência < 30ms para usuários no Brasil
  • SLA: 99,9% uptime (Plano Pro)
  • Auto-scaling de conexões pelo pgBouncer (pooling)
  • Backups automáticos diários

Configuração de DNS e domínio

# DNS Records (Registro.br)
CNAME  pipelinevendemmia.com.br  →  gueilee.github.io
A      pipelinevendemmia.com.br  →  185.199.108.153  (GitHub Pages IPs)
A      pipelinevendemmia.com.br  →  185.199.109.153
A      pipelinevendemmia.com.br  →  185.199.110.153
A      pipelinevendemmia.com.br  →  185.199.111.153

# Arquivo CNAME no repositório (raiz)
pipelinevendemmia.com.br

6.2 CI/CD Pipeline

O pipeline de CI/CD é extremamente simples: qualquer push para o branch main dispara o deploy automático via GitHub Actions, sem testes automatizados no momento.

# .github/workflows/pages.yml (implícito — GitHub Pages automático)

Trigger:  git push origin main
Build:    Nenhum (arquivos estáticos — sem bundler)
Test:     Nenhum (débito técnico — ver Fase 3)
Deploy:   GitHub Pages atualiza em 30-60 segundos
Rollback: git revert HEAD + push (ou revert via GitHub UI)

# Fluxo de deploy
Developer → git add + git commit → git push origin main
         → GitHub detecta push no main
         → GitHub Actions: Pages build job (~30s)
         → Akamai CDN invalida cache (~60s propagação global)
         → pipelinevendemmia.com.br atualizado

Branches e estratégia de versionamento

BranchPropósitoDeploy automático
mainBranch principal de produçãoSim → pipelinevendemmia.com.br
feature/*Desenvolvimento de novas funcionalidades (recomendado para evolução)Não
hotfix/*Correções urgentes em produçãoNão (merge manual para main)

Débito técnico — ausência de testes automatizados: Atualmente não há testes unitários, de integração ou E2E. Todo código vai direto para produção. Recomendado para evolução: implementar Playwright para testes E2E críticos (login, CRUD de lead, geração de PDF) executados no GitHub Actions antes do deploy.

6.3 Ambientes

Ambiente único (Production): Atualmente o sistema opera com apenas um ambiente — produção. Não há ambiente de desenvolvimento separado (DEV) ou homologação (HML). Esta é uma limitação do estágio atual que deve ser endereçada conforme o sistema escalar.

AmbienteStatusURLBanco de dados
Produção (PRD)✅ Ativopipelinevendemmia.com.brSupabase geojgcncmtbjebftosor (sa-east-1)
Homologação (HML)❌ Não existeRecomendado: Supabase projeto separado
Desenvolvimento (DEV)⚠ Localfile:///index.htmlMesmo banco de PRD (risco)

Estratégia de ambiente recomendada (evolução)

// Separação de ambientes via variável de configuração
const ENV = {
  production: {
    supabaseUrl: "https://geojgcncmtbjebftosor.supabase.co",
    supabaseKey: "eyJ..."  // anon key PRD
  },
  staging: {
    supabaseUrl: "https://STAGING_PROJECT.supabase.co",
    supabaseKey: "eyJ..."  // anon key HML
  }
};

// Detectar ambiente via hostname
const isProd = location.hostname === 'pipelinevendemmia.com.br';
const config = isProd ? ENV.production : ENV.staging;

6.4 Monitoramento e Observabilidade

📊
Supabase Dashboard
Monitoramento nativo
  • API Logs: Todas as chamadas REST em tempo real com latência e status
  • Auth Logs: Eventos de login/logout/falhas
  • Database: Conexões ativas, queries lentas, tamanho do banco
  • Storage: Uso total, arquivos por bucket
  • Alertas: Via Supabase Dashboard (e-mail quando atingir limites do plano)
Lacunas de observabilidade
Itens sem monitoramento
  • Sem APM (Application Performance Monitoring) no frontend
  • Sem tracking de erros JS (ex: Sentry)
  • Sem métricas de uso por usuário/feature
  • Sem alertas proativos de disponibilidade (UptimeRobot/Pingdom)
  • Recomendado: Sentry.io (free tier) para captura de erros JS + UptimeRobot para monitoramento de URL

6.5 Backup e Disaster Recovery

ComponenteEstratégia de backupRetençãoRTO estimadoRPO estimado
Banco de dados (PostgreSQL)Supabase: backup automático diário + PITR (Point-in-Time Recovery)7 dias (Pro)< 30 minutos24 horas (diário)
Storage (arquivos)Supabase Storage replicado internamente — sem backup externoIndefinido (enquanto conta ativa)< 1 horaSem backup externo
Código-fonteGitHub com histórico completo de commitsPermanente< 5 minutosÚltimo commit
Frontend (CDN)GitHub Pages reconstrói automaticamente de qualquer commit< 2 minutosÚltimo commit

Procedimento de Disaster Recovery

# Cenário 1: Banco de dados corrompido / dado excluído incorretamente
1. Acessar Supabase Dashboard → Settings → Backups
2. Selecionar backup do dia anterior (ou PITR para ponto específico)
3. Clicar "Restore" → confirmar
4. Aguardar restauração (~15-30 minutos)
5. Verificar integridade dos dados críticos
# RTO: ~30 min | RPO: até 24h de dados

# Cenário 2: Deploy com bug crítico em produção
1. git log --oneline -5  # identificar último commit bom
2. git revert HEAD       # ou revert de múltiplos commits
3. git push origin main  # deploy automático do revert
4. GitHub Pages atualiza em ~60 segundos
# RTO: ~2 min | RPO: 0 (dado não é afetado)

# Cenário 3: Supabase indisponível
1. Frontend carrega normalmente (estático no CDN)
2. Login falha — usuários não conseguem autenticar
3. Dados não carregam — sistema operacionalmente inativo
4. Aguardar recovery do Supabase (SLA 99,9%)
# RTO: depende do Supabase | RPO: 0 (sem escrita durante outage)
⚙ Especificação Técnica — Parte 2 de 3 concluída
Esta é a Parte 2. Acesse as demais partes pelos links da barra lateral.
✅ Documento aprovado
PIPELINE by Vendemmia
ET-LIC-2026-001 · Versão 1.0 · Fase 2/3 · APIs, Segurança e Infraestrutura
Confidencial — Vendemmia Comércio Internacional Ltda.
pipelinevendemmia.com.br · Maio 2026