Guilherme Gomes
← Todos os casos

Telemetria e gestão
de iluminação pública

Uma plataforma que monitora, comanda e presta contas do parque de iluminação de um município inteiro — do controlador no topo do poste até o relatório que a prefeitura recebe em PDF.

Cliente
Vegalux · município de Açailândia (MA)
Papel
Tech Lead, arquitetura e desenvolvimento
Período
2024 — presente
Situação
Em produção, uso diário

O problema

Gestão de iluminação pública, no modelo tradicional, é reativa: o cidadão liga reclamando de uma rua escura, alguém anota num caderno, uma equipe sai para verificar. Não há inventário confiável do parque, não há como provar consumo, e não há histórico de manutenção. O contrato com o município, porém, exige exatamente isso — indicadores, prazos de atendimento e prestação de contas.

O sistema precisava resolver quatro coisas ao mesmo tempo, cada uma com requisitos de tempo muito diferentes: telemetria contínua dos controladores, operação de campo por equipes com conectividade instável, atendimento ao cidadão em canal assíncrono (WhatsApp), e relatórios pesados que podem levar minutos para renderizar. Misturar tudo num único fluxo síncrono era garantia de derrubar a API.

Arquitetura

A separação central é entre o caminho quente (telemetria e comandos, que precisam ser rápidos e nunca bloquear) e o caminho frio (relatórios e processamento em lote, que podem demorar desde que não travem ninguém). Filas fazem a fronteira entre os dois.

CAMPO / IOT Controladores de poste estado, falha, corrente Medidores de consumo consumo do parque Cidadão reclamação por WhatsApp INGESTÃO Broker MQTT pub/sub bidirecional Webhook Twilio sessões de conversa APLICAÇÃO · NODE.JS + TYPESCRIPT CompanyResolver middleware de tenant por requisição Controllers HTTP REST · autenticação JWT Camada de serviços regra de negócio isolada TypeORM entidades com company_id Workers Bull relatórios · Puppeteer · chunking redimensiona o dyno sob carga CLIENTES Painel web React + Vite + Mapbox App de campo Expo · ordens de serviço App legado mesma API, contrato antigo Relatórios em PDF link assinado do storage PERSISTÊNCIA PostgreSQL tenant por coluna Redis fila de jobs Object storage PDFs e fotos LEGENDA caminho quente · telemetria e comando em tempo real caminho frio · operação, atendimento e relatórios fronteira de processo telemetria API única 2 contratos
Fluxo âmbar: caminho quente da telemetria. Fluxo cinza: caminho frio de operação e relatórios. A fronteira entre eles é a fila.

Decisões que definiram o sistema

Decisão 01

Multi-tenancy por coluna, resolvido em middleware

Contexto
O sistema nasceu para uma empresa só. Ao precisar atender várias, havia a opção de banco por tenant, schema por tenant ou discriminador por linha.
Escolha
Coluna company_id nas entidades, com um CompanyResolver que determina o tenant a cada requisição a partir do token e do cabeçalho — nunca por fallback implícito.
Consequência
Operação simples (um banco, uma migration) e custo baixo, ao preço de disciplina: toda query precisa passar pelo filtro. O isolamento virou responsabilidade da camada de acesso a dados, não do banco.
Decisão 02

Nunca assumir "a primeira empresa do usuário"

Contexto
A tentação óbvia num sistema recém-multi-tenant é, na ausência de contexto, usar usuario.empresas[0]. Isso resolve o compilador e cria um bug silencioso de vazamento entre tenants.
Escolha
Ausência de tenant é erro explícito, não valor padrão. O app legado, que não envia o cabeçalho, foi tratado por um caminho de compatibilidade declarado — não por acidente.
Consequência
Mais código de borda, e uma classe inteira de bug de isolamento que deixa de existir.
Decisão 03

Relatórios como jobs, não como requisições

Contexto
Os relatórios de modernização, custo, ordens de serviço e parque total renderizam com Puppeteer e, em intervalos longos de data, estouravam o tempo limite da requisição e a memória do dyno.
Escolha
Toda geração vai para uma fila Bull. O worker processa em blocos, salva o arquivo no object storage e devolve uma URL assinada. A API responde imediatamente com o identificador do job.
Consequência
A API deixou de cair sob relatório grande. Em troca, foi preciso construir estado de job, notificação de conclusão e política de retentativa.
Decisão 04

Compatibilidade retroativa como requisito, não como cortesia

Contexto
Eletricistas usam o app antigo em serviço, todos os dias. Não havia janela para migração coordenada nem para "atualize o aplicativo antes de sair".
Escolha
Os mesmos endpoints atendem dois contratos. Mudanças de comportamento — como a classificação de tipo de movimentação que passou a impedir consumo ilimitado de material em estoque — foram introduzidas de forma que a resposta ao cliente antigo permanecesse válida.
Consequência
Débito técnico assumido e documentado, com data para ser cobrado, em vez de uma quebra em campo no meio do expediente.

O que o sistema entrega hoje

  • Mapa operacional com carregamento por área visível: os pontos são buscados por bounding box, com debounce e limite de zoom, para não trazer o parque inteiro a cada arrasto do mapa.
  • Ordens de serviço com ciclo de vida completo, vinculadas a equipes, com registro de conclusão e localização.
  • Atendimento via WhatsApp com contagem correta de conversas não lidas — depois de reescrever o contador para considerar conversas distintas em vez de mensagens, o que inflava o badge com sessões de bot e sessões órfãs.
  • Relatórios per-tenant com identidade visual de cada empresa aplicada na renderização.
  • Logging estruturado, índices de banco revisados e análise estática contínua no pipeline.

O que eu faria diferente

A troca de empresa no painel web passou por três implementações antes de estabilizar. A causa raiz era conhecida — a função de recuperação do usuário devolvia uma nova referência de objeto a cada chamada, realimentando o efeito que dependia dela — e a solução final foi um recarregamento explícito da página. Funciona, é previsível, e é honestamente uma capitulação: o correto teria sido memoizar o contexto de autenticação desde o início, em vez de tratar o sintoma três vezes.

A segunda lição foi sobre backfill. A migração multi-tenant assumiu que os dados históricos já tinham tenant. Não tinham: postes, pontos e registros de consumo estavam todos com company_id nulo, e só as ordens de serviço estavam corretas. Verificar o estado real da produção antes de escrever a migration teria evitado o susto.