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
| Camada | Tecnologia |
|---|---|
| Backend | Rust, Axum 0.8 e Tokio |
| Banco | PostgreSQL com SQLx 0.8 (macros checadas em tempo de compilação e migrations), no schema termoleads |
| Importação | reqwest (download pelo WebDAV da Receita), scraper (leitura do XML do WebDAV), zip e csv (leitura tolerante dos arquivos) |
| Autenticação | JWT do TermoAuth (EdDSA), validado com jsonwebtoken contra o JWKS do serviço |
| Frontend | SvelteKit com Svelte 5, bits-ui e Tailwind CSS 4, publicado com adapter-static |
| Documentação | Sveltepress sobre SvelteKit, nesta pasta docs/projeto, publicada como site estático no Cloudflare Pages |
| Pacotes do frontend e da documentação | bun |
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/ 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
| Rota | Acesso | O que faz |
|---|---|---|
GET /healthz | Pública | Responde ok. É o que o deploy confere para saber se o backend subiu. |
GET /api/cnpj/{cnpj} | Pública | Ficha completa de um CNPJ (operação buscarPeloId). Aceita o número com ou sem máscara. |
GET /api/empresas | Login | Lista paginada com filtros (operação buscarPaginador). |
GET /api/auth/eu | Login | O usuário do token, com a flag master. |
POST /api/importar/iniciar | Master | Dispara a importação em segundo plano. |
POST /api/importar/cancelar | Master | Pede o cancelamento da importação em andamento. |
GET /api/importar/status | Master | O progresso atual da importação. |
GET /api/importar/relatorios | Master | As ú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_URLou 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 termoleadse fixa osearch_pathnesse schema em toda conexão do pool. Tabelas e o controle_sqlx_migrationsficam lá, nunca nopublic, 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 porCOPYnuma ú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.
| Rota | Tela |
|---|---|
/ | Porta de entrada: segue para /app. Só mostra conteúdo quando a renovação do token falhou (veja abaixo). |
/app | Consultar CNPJ. Com ?cnpj= na URL, a ficha já abre consultada. |
/app/consulta-empresas | Localizar Empresas. |
/master | Atalho para /master/dados-receita. |
/master/dados-receita | Dados 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
401do 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
isMasterdo 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 claimrolenão dá acesso master. - No backend,
/api/importar/*passa pelo middlewareexigir_master: sem token válido é401, com token de usuário comum é403. - No frontend, o guard de
/mastermanda quem não é master para/app. Isso é conveniência de navegação; a proteção de verdade é a do backend.
Configuração
| Variável | Onde | Para que serve |
|---|---|---|
APP_ENV | Backend | production ou prod carrega .env.prod; qualquer outro valor carrega .env.local. |
DATABASE_URL | Backend | Conexão com o PostgreSQL. Obrigatória. |
DATABASE_MAX_CONNECTIONS | Backend | Tamanho do pool (padrão 10; a sandbox usa 5). |
DATABASE_ACQUIRE_TIMEOUT | Backend | Segundos de espera por uma conexão livre (padrão 5). |
PORT | Backend | Porta HTTP (padrão 51031). |
TERMOAUTH_URL | Backend | Endereço do frontend do TermoAuth, esperado em iss e aud (padrão http://localhost:51000). |
TERMOAUTH_API_URL | Backend | Endereço do backend do TermoAuth, de onde sai o JWKS (padrão http://localhost:51001). |
DOWNLOAD_DIR | Backend | Pasta dos arquivos baixados e das partições da importação (padrão downloads). |
CNPJ_CONCURRENCIA | Backend | Quantos arquivos baixar ao mesmo tempo da Receita (padrão 3). |
PUBLIC_API_BASE_URL | Frontend | Endereço do backend. |
PUBLIC_AUTH_URL | Frontend | Endereço do frontend do TermoAuth. |
Rodando localmente
As portas seguem a faixa do TermoLeads no padrão da Termotubos, 51030 a 51039.
| Serviço | Porta | Como subir |
|---|---|---|
| Frontend | 51030 | Configuração frontend do .claude/launch.json (bun run dev em frontend) |
| Backend | 51031 | Configuração backend do .claude/launch.json (cargo run em backend) |
| Documentação | 51032 | bun run dev em docs/projeto |
| Diagrama do banco | 51033 | bun run dev em docs/database-erd |
| Drizzle Studio | 51034 | bun run dev em docs/database-studio |
| PostgreSQL | 5432 | Serviç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.
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.
| Workflow | Branch | O que faz |
|---|---|---|
deploy-sandbox-backend.yml | sandbox | Compila 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.yml | producao | Publicaçã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.