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
| Tabela | Métodos usados | Filtros principais | Observação |
lic_auditoria | GET, POST, PATCH | lead_id=eq.{id} | UNIQUE por lead_id — máx. 1 registro por lead |
lic_performance | GET, POST | lead_id=eq.{id}&order=periodo_ref.asc | Múltiplos por lead (histórico trimestral) |
lic_historico | GET, POST | lead_id=eq.{id}&order=created_at.desc | Append-only; sem UPDATE nem DELETE no uso normal |
lic_anexos | GET, POST, DELETE | lead_id=eq.{id} ou tipo=like.foto_armazem% | DELETE também remove o arquivo do Storage |
4.2 Supabase Auth API
| Endpoint | Método | Função | SDK call |
/auth/v1/token?grant_type=password | POST | Login com e-mail + senha → retorna JWT | supabaseClient.auth.signInWithPassword() |
/auth/v1/logout | POST | Logout — invalida o refresh token | supabaseClient.auth.signOut() |
/auth/v1/token?grant_type=refresh_token | POST | Refresh do JWT (automático pelo SDK) | Automático |
/auth/v1/recover | POST | Envio de e-mail de reset de senha | supabaseClient.auth.resetPasswordForEmail() |
/auth/v1/user | PUT | Atualizaçã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ção | Endpoint | Método | Autenticaçã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} | GET | Nenhuma (bucket público) |
| Obter URL pública | SDK: getPublicUrl(path) | — | Operação local (sem chamada de rede) |
| Deletar arquivo | /storage/v1/object/lic-anexos (body: paths[]) | DELETE | JWT authenticated |
| Listar arquivos | /storage/v1/object/list/lic-anexos | POST (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
| API | Endpoint | Uso | Auth | Fallback |
| BrasilAPI v2 | https://brasilapi.com.br/api/cep/v2/{cep} | Geocodificação por CEP (lat/lng + endereço) | Nenhuma | ViaCEP |
| ViaCEP | https://viacep.com.br/ws/{cep}/json/ | Endereço por CEP (sem coordenadas) | Nenhuma | Mensagem de erro |
| CartoDB Voyager | https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png | Tiles do mapa interativo | Nenhuma | Mapa em branco |
| GitHub GeoJSON | https://raw.githubusercontent.com/giuliano-macedo/geodata-br-states/main/geojson/br_states.json | Polígonos dos estados para choropleth | Nenhuma | Sem choropleth |
| QR Server API | https://api.qrserver.com/v1/create-qr-code/?size=280x280&data={url} | Geração de QR Code para link do portal | Nenhuma | Link 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 HTTP | Código PostgREST | Significado | Ação no frontend |
| 200 | — | Sucesso | Processar data[] |
| 201 | — | Criado com sucesso | Toast verde + fechar modal |
| 204 | — | Atualizado/Deletado sem body | Toast verde |
| 400 | PGRST100 | Sintaxe inválida na query | Log console + toast erro |
| 401 | PGRST301 | JWT ausente ou inválido | Redirecionar para login |
| 403 | PGRST302 | RLS bloqueou | Toast "Acesso negado" |
| 404 | — | Registro não encontrado | Toast "Não encontrado" |
| 409 | PGRST204 | Conflito de constraint (UNIQUE) | Toast com mensagem específica |
| 422 | PGRST203 | Violação NOT NULL ou CHECK | Toast com campo inválido |
| 500+ | — | Erro interno Supabase | Toast "Erro interno, tente novamente" |
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
| Tabela | Política atual | Risco | Evolução recomendada |
lic_leads | ALL USING (true) — permissiva | Médio | Restringir por auth.email() = ANY(whitelist) |
lic_auditoria | ALL USING (true) — permissiva | Médio | Idem |
lic_performance | ALL USING (true) — permissiva | Médio | Idem |
lic_historico | ALL USING (true) — permissiva | Médio | SELECT para todos autenticados; INSERT apenas para autores |
lic_anexos | ALL USING (true) — permissiva | Médio | INSERT 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 upload | Extensões aceitas | Tamanho máx. |
| Contrato Social | .pdf, .jpg, .jpeg, .png, .doc, .docx | 10 MB |
| Fotos de armazéns | .jpg, .jpeg, .png, .heic, .webp | 10 MB cada |
| Certificados | .pdf, .jpg, .jpeg, .png | 10 MB cada |
| Relatório de visita técnica | .pdf, .doc, .docx | 20 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 dado | Exemplos | Base legal (LGPD) | Medidas implementadas |
| Dados pessoais | Nome do contato, e-mail, telefone, cargo | Legítimo interesse (prospecção B2B) | HTTPS em trânsito; RLS no banco; acesso restrito via whitelist |
| Dados empresariais | CNPJ, razão social, faturamento, armazéns | Execução de contrato / legítimo interesse | Mesmas medidas; tratados como confidenciais |
| Documentos | Contrato Social, certificados, fotos | Execução de contrato | Supabase Storage; URLs públicas mas sem indexação; acesso apenas por URL direta |
| Dados de comportamento | Histórico de eventos, KPIs | Legítimo interesse (gestão operacional) | Armazenados no banco com RLS; sem analytics externo |
| Credenciais | E-mail e senha dos usuários internos | — | Gerenciado 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
| Vulnerabilidade | Status | Mitigação atual | Risco residual |
| XSS (Cross-Site Scripting) | Mitigado | Template literals com interpolação direta de dados do banco — sem innerHTML com dados não sanitizados em campos críticos | Baixo — dados do banco são inseridos em contexto HTML conhecido |
| SQL Injection | Mitigado | Toda comunicação via SDK Supabase com queries parametrizadas; PostgREST nunca executa SQL dinâmico | Nenhum |
| CSRF | Mitigado | SPA com JWT Bearer token no header (não cookie) — CSRF não aplicável para Bearer auth | Nenhum |
| Exposição da anon key | Aceitável | A anon key é projetada para ser pública — acesso real controlado pelo RLS e pelo JWT. Não é a service_role key | Baixo — documentado pelo Supabase como seguro para cliente |
| Path traversal em Storage | Mitigado | Sanitização do nome do arquivo + path prefixado por lead_id | Baixo |
| RLS permissivo | Débito técnico | Política permissiva atual; whitelist no frontend como controle principal | Médio — endereçar na evolução com RLS por e-mail |
| localStorage (JWT) | Aceitável | Padrão do Supabase SDK; alternativa (httpOnly cookie) requer backend próprio | Baixo no contexto B2B interno |
| Upload de malware | Parcial | Extensões restritivas no frontend; Storage não escaneia vírus | Médio — evoluir com ClamAV via Edge Function |
5.7 Logs e Auditoria
| Fonte de log | Onde acessar | Retenção | O que captura |
| Supabase API logs | Dashboard → Logs → API | 7 dias (Pro) | Todas as chamadas REST: endpoint, método, status, latência, IP |
| Supabase Auth logs | Dashboard → Logs → Auth | 7 dias | Logins, logouts, resets de senha, falhas de autenticação |
| Supabase Postgres logs | Dashboard → Logs → Postgres | 7 dias | Queries SQL lentas (>1s), erros de constraint, deadlocks |
| lic_historico (app-level) | Tabela no banco | Permanente | Eventos de negócio: mudanças de etapa, KPIs, certificados, reuniões |
| GitHub Actions | github.com/Gueilee/pipeline-comercial | 90 dias | Deploys: 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
| Branch | Propósito | Deploy automático |
main | Branch principal de produção | Sim → pipelinevendemmia.com.br |
feature/* | Desenvolvimento de novas funcionalidades (recomendado para evolução) | Não |
hotfix/* | Correções urgentes em produção | Nã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.
| Ambiente | Status | URL | Banco de dados |
| Produção (PRD) | ✅ Ativo | pipelinevendemmia.com.br | Supabase geojgcncmtbjebftosor (sa-east-1) |
| Homologação (HML) | ❌ Não existe | — | Recomendado: Supabase projeto separado |
| Desenvolvimento (DEV) | ⚠ Local | file:///index.html | Mesmo 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
| Componente | Estratégia de backup | Retenção | RTO estimado | RPO estimado |
| Banco de dados (PostgreSQL) | Supabase: backup automático diário + PITR (Point-in-Time Recovery) | 7 dias (Pro) | < 30 minutos | 24 horas (diário) |
| Storage (arquivos) | Supabase Storage replicado internamente — sem backup externo | Indefinido (enquanto conta ativa) | < 1 hora | Sem backup externo |
| Código-fonte | GitHub com histórico completo de commits | Permanente | < 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