Arquitetura

O TermoLeads tem duas partes: um backend em Rust que guarda a base CNPJ no PostgreSQL e faz a importação, e um frontend SvelteKit publicado como site estático. O login fica fora dos dois, no TermoAuth.

Stack

CamadaTecnologia
BackendRust, Axum 0.8 e Tokio
BancoPostgreSQL com SQLx 0.8 (macros checadas em tempo de compilação e migrations), no schema termoleads
Importaçãoreqwest (download pelo WebDAV da Receita), scraper (leitura do XML do WebDAV), zip e csv (leitura tolerante dos arquivos)
AutenticaçãoJWT do TermoAuth (EdDSA), validado com jsonwebtoken contra o JWKS do serviço
FrontendSvelteKit com Svelte 5, bits-ui e Tailwind CSS 4, publicado com adapter-static
DocumentaçãoSveltepress sobre SvelteKit, nesta pasta docs/projeto, publicada como site estático no Cloudflare Pages
Pacotes do frontend e da documentaçãobun

Organização

├── backend/
│   ├── migrations/
│   │   ├── 20260922000001_criar_base_cnpj.sql
│   │   └── 20260922000002_criar_importacao.sql
│   └── src/
│       ├── main.rs
│       ├── lib.rs
│       ├── bin/
│       │   └── sanear_teste.rs
│       ├── controller/
│       │   ├── auth/
│       │   │   └── eu.rs
│       │   ├── cnpj/
│       │   │   ├── buscarPeloId.rs
│       │   │   └── buscarPaginador.rs
│       │   └── importar/
│       │       ├── baixarCNPJReceitaFederal.rs
│       │       └── relatorioCNPJReceitaFederal.rs
│       ├── model/
│       │   ├── cnpj/
│       │   └── importar/
│       ├── router/
│       │   ├── auth.rs
│       │   ├── cnpj.rs
│       │   └── importar.rs
│       └── helper/
│           ├── auth.rs
│           ├── db.rs
│           ├── jwt.rs
│           ├── paths.rs
│           └── state.rs
├── frontend/
│   ├── src/lib/
│   │   ├── auth/
│   │   ├── hooks/
│   │   ├── modules/cnpj/
│   │   └── modules/importacao/
│   └── src/routes/
│       ├── app/
│       └── master/
└── docs/
    ├── projeto/
    ├── database-erd/
    └── database-studio/
txt
Mostrar código completo

Hoje existem duas features: cnpj (a consulta) e importar (a carga da base). O pacote Rust se chama cnpj-dashboard, e o mesmo núcleo (lib.rs) serve o servidor e a ferramenta de diagnóstico sanear_teste, que lê um .zip da Receita com o mesmo leitor da importação e mostra quantas linhas são válidas e quantas seriam descartadas.

Backend

Rotas

RotaAcessoO que faz
GET /healthzPúblicaResponde ok. É o que o deploy confere para saber se o backend subiu.
GET /api/cnpj/{cnpj}PúblicaFicha completa de um CNPJ (operação buscarPeloId). Aceita o número com ou sem máscara.
GET /api/empresasLoginLista paginada com filtros (operação buscarPaginador).
GET /api/auth/euLoginO usuário do token, com a flag master.
POST /api/importar/iniciarMasterDispara a importação em segundo plano.
POST /api/importar/cancelarMasterPede o cancelamento da importação em andamento.
GET /api/importar/statusMasterO progresso atual da importação.
GET /api/importar/relatoriosMasterAs últimas 50 execuções de importação.

A consulta por CNPJ é pública na API para permitir consulta por integração, mas a tela que a usa fica dentro da área logada.

Banco

  • Um banco só, obrigatório. Sem DATABASE_URL ou com o PostgreSQL fora do ar, o backend encerra ao subir com a mensagem do motivo.
  • Schema próprio. Ao conectar, o backend roda CREATE SCHEMA IF NOT EXISTS termoleads e fixa o search_path nesse schema em toda conexão do pool. Tabelas e o controle _sqlx_migrations ficam lá, nunca no public, porque vários apps da Termotubos podem dividir o mesmo banco.
  • Migrations no startup. As migrations de backend/migrations/ são aplicadas pelo próprio backend (sqlx::migrate!) toda vez que ele sobe; só roda o que ainda não foi aplicado.
  • Pool de 10 conexões por padrão (DATABASE_MAX_CONNECTIONS) e espera de 5 s por conexão (DATABASE_ACQUIRE_TIMEOUT): o cluster da DigitalOcean é dividido por todos os apps da Termotubos, então cada workflow de deploy define o tamanho do pool do seu ambiente (a sandbox usa 5). A montagem da importação grava por COPY numa única conexão dedicada.
  • As tabelas estão descritas em Base CNPJ .

Estado da aplicação

O AppState carrega três coisas: o pool do PostgreSQL, o progresso da importação em memória (é ele que o /status devolve) e o validador de token do TermoAuth com o JWKS em cache. O progresso vive só na memória do processo: reiniciar o backend durante uma importação a interrompe, e o histórico no banco fica com a execução como executando.

CORS

O CORS está aberto para qualquer origem, sem credenciais. O token viaja no cabeçalho Authorization, não em cookie.

Frontend

O frontend é uma SPA estática: adapter-static com página de fallback 200.html (ou index.html quando o build roda no Cloudflare Pages). Não há código de servidor do SvelteKit; toda a lógica roda no navegador e fala com o backend pela URL em PUBLIC_API_BASE_URL.

RotaTela
/Porta de entrada: segue para /app. Só mostra conteúdo quando a renovação do token falhou (veja abaixo).
/appConsultar CNPJ. Com ?cnpj= na URL, a ficha já abre consultada.
/app/consulta-empresasLocalizar Empresas.
/masterAtalho para /master/dados-receita.
/master/dados-receitaDados da Receita Federal: importação e histórico.

Cada módulo de lib/modules tem um contexto que é o único lugar que chama a API: ContextoCnpj.ts para a consulta e contexto_importacao.svelte.ts para a importação.

Autenticação

O TermoLeads não autentica ninguém: quem faz o login é o TermoAuth. O TermoLeads está no registro do TermoAuth como termo-leads, com caminho inicial /app e aberto só ao perfil colaborador.

1. Sem token, vai para o TermoAuth

O guard de /app e de /master confere se há token no sessionStorage. Sem token, guarda a tela atual e manda o navegador para PUBLIC_AUTH_URL/handoff?app=termo-leads.

2. O TermoAuth devolve o token no fragmento

Depois do login (ou na hora, se a sessão central já estiver ativa), o TermoAuth devolve o navegador para /app#token=.... O layout raiz lê o token do fragmento, guarda no sessionStorage, limpa a URL e leva a pessoa de volta para a tela de onde ela saiu.

3. Todo pedido ao backend leva o token

Um interceptor do fetch acrescenta Authorization: Bearer a toda chamada para o backend. O guard chama GET /api/auth/eu para confirmar que o token funciona antes de mostrar a tela.

4. O backend confere a assinatura

O backend valida o JWT com EdDSA contra a chave pública do JWKS do TermoAuth (TERMOAUTH_API_URL/.well-known/jwks.json) e confere iss, aud (iguais a TERMOAUTH_URL) e a validade. O JWKS fica em cache; um token com kid desconhecido faz o cache ser recarregado uma vez antes de ser recusado.

Renovação e saída

  • O token vale 1 hora e não há refresh token. Um 401 do backend apaga o token e refaz a ida ao /handoff; com a sessão central ativa, a volta é imediata.
  • Disjuntor contra loop. Se a renovação falhar três vezes em 90 segundos, o frontend para de redirecionar e mostra na raiz a mensagem "Não foi possível validar sua sessão", com um botão para entrar de novo.
  • Sair apaga o token e manda para PUBLIC_AUTH_URL/sign-out?app=termo-leads, que encerra a sessão central.

Master

  • O acesso master vem só do claim isMaster do token, que o TermoAuth preenche a partir da lista de masters dele. O TermoLeads não tem lista de e-mails nem guarda essa flag. O claim role não dá acesso master.
  • No backend, /api/importar/* passa pelo middleware exigir_master: sem token válido é 401, com token de usuário comum é 403.
  • No frontend, o guard de /master manda quem não é master para /app. Isso é conveniência de navegação; a proteção de verdade é a do backend.

Configuração

VariávelOndePara que serve
APP_ENVBackendproduction ou prod carrega .env.prod; qualquer outro valor carrega .env.local.
DATABASE_URLBackendConexão com o PostgreSQL. Obrigatória.
DATABASE_MAX_CONNECTIONSBackendTamanho do pool (padrão 10; a sandbox usa 5).
DATABASE_ACQUIRE_TIMEOUTBackendSegundos de espera por uma conexão livre (padrão 5).
PORTBackendPorta HTTP (padrão 51031).
TERMOAUTH_URLBackendEndereço do frontend do TermoAuth, esperado em iss e aud (padrão http://localhost:51000).
TERMOAUTH_API_URLBackendEndereço do backend do TermoAuth, de onde sai o JWKS (padrão http://localhost:51001).
DOWNLOAD_DIRBackendPasta dos arquivos baixados e das partições da importação (padrão downloads).
CNPJ_CONCURRENCIABackendQuantos arquivos baixar ao mesmo tempo da Receita (padrão 3).
PUBLIC_API_BASE_URLFrontendEndereço do backend.
PUBLIC_AUTH_URLFrontendEndereço do frontend do TermoAuth.

Rodando localmente

As portas seguem a faixa do TermoLeads no padrão da Termotubos, 51030 a 51039.

ServiçoPortaComo subir
Frontend51030Configuração frontend do .claude/launch.json (bun run dev em frontend)
Backend51031Configuração backend do .claude/launch.json (cargo run em backend)
Documentação51032bun run dev em docs/projeto
Diagrama do banco51033bun run dev em docs/database-erd
Drizzle Studio51034bun run dev em docs/database-studio
PostgreSQL5432Serviço local do PostgreSQL; .claude/preparar-banco.sh cria o banco e aplica as migrations

O TermoAuth precisa estar rodando em http://localhost:51000 (frontend) e http://localhost:51001 (backend) para o login funcionar.

A importação precisa de espaço em disco

Antes de montar a base, a importação confere o espaço livre na pasta de download: as partições temporárias ocupam o tamanho dos arquivos descompactados mais 10%. Sem espaço, ela para com uma mensagem pedindo para liberar disco ou apontar DOWNLOAD_DIR para outro volume.

Publicação

Os workflows de .github/workflows/ publicam cada parte separadamente.

WorkflowBranchO que faz
deploy-sandbox-backend.ymlsandboxCompila o backend com SQLX_OFFLINE (o cache .sqlx/ substitui o banco na compilação), envia o binário e sobe o serviço termoleads-backend na porta 51031, com DOWNLOAD_DIR em /var/lib/termoleads-backend/downloads. Confere /healthz antes de dar o deploy por concluído.
deploy-backend.ymlproducaoPublicação de produção do backend em servidor Oracle. Ainda usa os nomes antigos do projeto (Nav Leads).

O frontend não tem workflow: é publicado pelo Cloudflare Pages.

No sandbox, o frontend responde em https://sandbox-leads.termotubos.com.br (entrada em /app) e a API em https://sandbox-api-leads.termotubos.com.br, usando o TermoAuth do sandbox. Em produção os endereços são os mesmos sem o sandbox-: https://leads.termotubos.com.br e https://api-leads.termotubos.com.br.

Atualizado em 2026/10/10 15:04