Como eu levei um distribuidor de hortifrúti do papel e da calculadora até um sistema operacional próprio, da pesquisa de campo à segurança em produção.
Antes · o quadro de avisos
Depois · a Fila de Pedidos
Todo mundo em 2026 tem um projeto feito com IA. Quase todos são genéricos, e quase nenhum tem um usuário.
Este é específico. Foi desenhado para uma operação real, com nome, endereço e funcionários cadastrados. E o gancho que me interessa não é que eu usei IA para construí-lo. É que, tendo IA disponível de graça no meu editor, eu decidi não colocar nenhuma IA dentro do produto. É a decisão que eu mais gosto de defender.
Antes dela, o caminho inteiro: como eu descobri o problema, o que a pesquisa revelou que ninguém tinha pedido para resolver, como as decisões foram tomadas e registradas, e o que existe hoje de código, de banco e de segurança.
O NR compra hortifrúti no CEASA de Curitiba e revende em Guarapuava para supermercados, restaurantes, hotéis e pessoa física. O diferencial competitivo dele é logística própria até o CEASA, o que garante frescor e variedade que o concorrente local não alcança.
A operação inteira, da compra à entrega, rodava em papel, calculadora, WhatsApp e planilha. O estoque era “pelo olho”.
E ela tem duas características que definem tudo o que veio depois.
Variabilidade extrema. O preço de um produto muda em quatro horas. A safra muda a qualidade. Um fornecedor some. O peso de uma caixa varia. O mesmo produto é comprado em caixa e vendido em quilo, unidade ou caixa fechada, dependendo do cliente.
Conhecimento na cabeça das pessoas. O que comprar, quanto comprar, qual cliente aceita qual produto, qual horário entregar, o que “abacaxi 2” significa para aquele cliente específico. Nada disso estava escrito em lugar nenhum.
Uma operação assim não cabe num molde pronto. Foi daí que saiu a régua do projeto inteiro.
A operação da empresa não deve mudar. O sistema deve mudar para a operação da empresa.
Não houve briefing. Houve galpão.
| Método | O que rendeu |
|---|---|
| Observação presencial recorrente |
Artefatos que ninguém menciona numa entrevista: a folha A4 do comprador, o quadro de avisos, a prancheta de assinatura, as 4 balanças. E os padrões que só aparecem vendo o trabalho acontecer:
|
| Entrevista por função comprador, atendente, separador, conferente, precificação | Cada persona do sistema saiu de uma pessoa real com um problema real |
| Transcrição de áudio | A lógica de precificação foi capturada assim: 4 regras de cálculo que só existiam na cabeça de uma pessoa |
| Análise de artefato físico | A planilha, o CSV, a folha do comprador |
| Análise do dado legado | 72.992 linhas de histórico de vendas do sistema antigo, importadas e analisadas |
| Documentação AS-IS / TO-BE | Dois documentos longos escritos antes de qualquer linha de código |
Na primeira conversa com o dono, eu não perguntei quais funcionalidades ele queria. Perguntei:
“O que acontece com a operação se você faltar amanhã?”
A resposta era: ela para. O comprador é o único que negocia, e negocia por WhatsApp com uma rede de fornecedores que só ele conhece. A atendente é a única que sabe traduzir o pedido informal de cada cliente. A conferente é a única que sabe qual cliente aceita entrega a que horas.
Isso reposicionou o projeto. O produto não é “um sistema de pedidos”. É a externalização do conhecimento tácito de uma operação inteira, sem tirar dela a flexibilidade que a mantém competitiva.
Estes são achados, não requisitos.
A atendente recebia por WhatsApp, escrevia à mão num papel e colava num quadro de avisos. O separador pegava o papel, coletava os produtos, ia até a balança e só ali criava o pedido no sistema, digitando o que ele mesmo tinha pesado. Entre o cliente pedir e o sistema saber existia um buraco de horas cuja única fonte de verdade era uma folha de papel.
A conferente pegava a folha impressa pelo separador e conferia os produtos contra o que o separador tinha pesado. Nunca contra o que o cliente tinha pedido.
Pare um segundo nisso. Se o separador esquecesse um item, a conferência batia perfeitamente, porque o item esquecido não estava na folha. Era uma etapa de controle de qualidade matematicamente incapaz de detectar a falha mais comum do processo. Ninguém no NR tinha percebido, porque de dentro do processo ele parece correto.
Esse é o achado mais importante do projeto, e ele não veio de nenhuma pergunta que me fizeram.
Não havia campo de prioridade. A atendente colava o papel mais urgente mais em cima no quadro, e os separadores deviam pegar de cima para baixo. A regra de negócio existia, implementada em fita adesiva.
Só que ninguém estava olhando. Como o quadro é físico e a escolha é livre, o separador podia pegar outro pedido: um mais fácil, ou um bem longo que não era prioritário. Um pedido longo é uma forma de ficar ocupado por muito tempo sem entregar o que era urgente.
Esse é o padrão que todo processo puramente físico sofre: onde existe brecha, alguém encontra o caminho de trabalhar menos. A prioridade no quadro não era ignorada por indisciplina, era ignorada porque nada no processo tornava a escolha visível.
As 4 balanças tinham um login compartilhado. Para saber quem separou um pedido, era preciso ir olhar a prancheta onde o separador escrevia o nome dele à mão.
Para pesar um pallet com 20 caixas, o separador contava as caixas, somava as taras na calculadora, somava a tara do pallet, subtraía do peso bruto e digitava o resultado. A cada produto. A cada pedido.
Este achado é contraintuitivo, e é o meu favorito depois da conferência cega. Eu entrei na conversa achando que o problema era “ele não sabe o estoque”. A resposta foi:
“Não importa. Se eu tiver 100 kg de batata e a batata estiver ruim, ele vai comprar do mesmo jeito.”
O comprador sempre olha o produto fisicamente antes de comprar, porque a decisão dele é sobre qualidade, não sobre saldo. Isso matou um requisito que parecia óbvio, o dashboard de estoque como gatilho de compra, e redefiniu o módulo inteiro: a requisição começa com ele olhando o galpão, e o sistema é o bloco de anotação, não o oráculo.
Toda a descoberta convergiu para uma frase, escrita assim no documento inicial:
“Nós temos regras e temos também costumes. Existem critérios que geralmente são construídos em ERPs e são boas práticas. Só que tem coisas que não dá para colocar no sistema. Não dá para ser tão perfeito quanto dizem os manuais. Precisamos fazer um sistema que abrace o fluxo deles e faça com que continuem trabalhando praticamente da mesma forma, só que muito mais fácil. Que não trave, que não faça o usuário parar e perder minutos lendo, aprendendo, tentando. Tem que ser quase instantâneo.”
O sistema abraça o fluxo de trabalho existente. Ele não força a operação a se moldar a ele.
Isso tem três consequências práticas.
(a) Digitalizar o artefato, não substituí-lo por um conceito novo. O quadro de avisos virou a Fila de Pedidos. O papel mais em cima virou o badge “Urgente”. A prancheta virou o registro de quem assumiu o pedido. O usuário reconhece o próprio processo na tela, e não precisa aprender um modelo mental novo.
(b) Onde o processo humano é o diferencial competitivo, o sistema fica de fora. A negociação do comprador com cerca de 50 fornecedores acontece por WhatsApp e vai continuar acontecendo por WhatsApp. Trazer isso para dentro, “agora ele entra e faz uma cotação”, quebraria a coisa que faz o negócio funcionar. O sistema começa depois da negociação, no instante em que ela vira uma instrução de compra.
Essa é a decisão mais madura do projeto: reconhecer onde o produto não deve entrar.
(c) O sistema corrige o que estava errado, mas só o que estava errado. A conferência cega foi mudada, porque estava estruturalmente quebrada. A negociação não foi tocada, porque estava certa.
A arquitetura de informação saiu da operação, não de um template de ERP.
| Esfera | Quem | O que acontece |
|---|---|---|
| Compras e Recebimento | Gerente, Comprador, Conferente de Recebimento | Define o que comprar, negocia fora do sistema, registra a compra e conta o caminhão. É aqui que o estoque nasce |
| Pedidos e Atendimento | Atendente | Recebe o pedido do cliente, cria no sistema, precifica |
| Galpão | Separador, Balança, Conferente de Entrega, Motorista | Separa, pesa, confere e entrega |
Cada persona foi definida por função + dispositivo + restrição física, nunca por cargo.
| Persona | Dispositivo | A restrição que moldou a interface |
|---|---|---|
| Atendente | Desktop, split-view | Trabalha com o WhatsApp aberto ao lado; precisa colar texto e transformar em pedido |
| Separador | Celular dedicado, travado no app | Uma mão ocupada, ambiente físico, aparelho travado no app |
| Balança | 4 terminais compartilhados | Não é uma pessoa, é o login da estação. Pessoas se identificam nela |
| Conferente de Entrega | Mobile/tablet na doca | Confere em pé, item a item, contra o pedido do cliente |
| Conferente de Recebimento | Mobile/tablet na doca | Conta caixas que chegam aos poucos, de um caminhão sendo descarregado |
| Motorista | Celular, na rua | Uma mão, tela pequena, sinal ruim, não pode ver preço nenhum |
| Gerente / Comprador | Desktop + mobile | Decide, não executa. Precisa de visão ampla |
| Administrador | Desktop + mobile | Acesso total |
Duas “personas” do sistema não são pessoas, e isso é resultado de design e não acidente: Balança é um perfil de estação física, e Desenvolvedor era um acesso somente-leitura para um integrador externo. Hoje são 10 perfis no total, com permissão por seção.
Atendente · desktop
Balança · terminal fixo
Separador · celular
Motorista · celular
Este é o item que eu mais destacaria, porque a maioria dos designers nunca faz.
Existe um documento de 563 linhas que é a fonte de verdade da linguagem do sistema. Ele define o nome exato de cada persona, com o que ela faz e o que ela explicitamente não faz; cada label de contexto; e as distinções que parecem sinônimos e não são.
Um exemplo real do que ele força:
“Retorno da conferência”: voltou da conferência por item reprovado.
“Retorno de entrega”: o pedido saiu e voltou inteiro, porque o cliente sumiu. A diferença que falta é perda.
“Entrega com divergência”: o cliente ficou com parte. A diferença é venda.
“Recoleta”: voltou porque faltou coletar, não porque algo foi reprovado.
Quatro situações que um sistema mal desenhado chamaria de “devolução”, e que significam coisas financeiramente opostas. A frase de abertura do documento resume a tese:
“Ambiguidade aqui causa bugs de entendimento antes de virar bug de código.”
Ele também lista termos proibidos. Não dizer “importar do WhatsApp”, porque não existe integração com WhatsApp: a atendente copia e cola, e o nome correto é Campo de Texto Livre. Não dizer “número diário”, porque para a operação existe um número de pedido. Não dizer “data da ocorrência” numa perda, porque esse campo existiu, criava duas verdades sobre o mesmo evento, e foi removido.
O catálogo não foi inventado. Peguei os 341 nomes de produto que a empresa efetivamente vendeu nos 4 meses anteriores e classifiquei um a um, gerando as regras de classificação:
BDJ/bandeja vira modo unidade: mesma base, variante de venda diferenteHB 623 - CAIXA PLÁSTICA não é produto, é embalagemE gerou uma lista de ambiguidades para validar com o cliente: “ALFACE CRESPA e ALFACE CRESPA UN parecem duplicatas”, “MURGOTE: identificar produto”. Levantar a dúvida em vez de adivinhar é parte do método, e é o que separa taxonomia de chute.
Hoje são 382 produtos cadastrados.
Esta é a decisão de modelagem que mais paga juros, e ela é de produto antes de ser de banco.
O mesmo tomate pode ser comprado em caixa de 20 kg e vendido por quilo para o restaurante, por bandeja para o mercado e por caixa fechada para o atacadista. Se “produto” for uma coisa só, você é obrigado a escolher: ou duplica o item e perde o estoque unificado, ou unifica e perde o preço por modo de venda.
O modelo separa as duas perguntas:
produto_base, o produto físico. O que entra e sai do estoque. Carrega estoque, custo, peso médio e os campos de compra. Um registro por produto real.produtos, o produto de venda. O que aparece no pedido do cliente. Carrega preço, margem, unidade de venda e o peso da bandeja. Pode haver vários por base.
25 kg · 50 cx · 250 un, porque a mesma mercadoria é comprada, guardada e vendida em grandezas diferentes.Custo, estoque e peso vivem na base. Preço e margem vivem no produto de venda.
Isso não é teoria: hoje, 73 produtos-base já têm mais de um produto de venda no cadastro, e os três modos de venda estão em uso: quilo, unidade e caixa.
O modo caixa fechou a matriz comprar × vender: comprar em kg, manter o estoque em kg e vender a caixa fechada. E ele obrigou uma segunda decisão, porque um produto vendido em caixa conta, nunca pesa. A tela de separação ramifica pelo modo de venda, não pela unidade, e grava contagem em vez de peso.
Há ainda um segundo regime, que resolve um bug silencioso: produtos que nunca se vendem em quilo, como ovos e bandejas. Antes, cadastrar um produto sem peso médio e comprar 20 caixas a R$ 50 gerava estoque 0 e custo nulo em silêncio, porque o recebimento calculava contagem × peso_medio, e peso_medio era zero. A correção foi tornar o regime explícito, nunca inferido da ausência de peso: ausência de peso é o bug, não um sinal de regime. Hoje 13 produtos-base rodam em regime contado, com estoque em unidades e custo em reais por unidade.
Um exemplo de decisão que só aparece quando você olha o dado real, e um dos meus favoritos, porque a solução “óbvia” estava ativamente errada.
Compra em kg. O caminhão chega. O conferente conta os volumes. A regra original creditava contagem × peso_medio.
Por que isso está errado: contar caixas não mede peso. 100 kg em caixas de 22 kg ocupam 5 volumes, sendo 4 cheios e 1 pela metade. O conferente conta 5, corretamente, e o sistema credita 5 × 22 = 110 kg. Dez por cento de estoque fantasma, e valor de estoque acima do que foi pago, em toda compra em kg. O dinheiro que saiu foi quantidade_solicitada × valor_unitário, e o estoque que entrou era outro número.
A decisão: dentro de uma faixa plausível, a verdade é o peso pedido. Fora dela, a verdade é a contagem. Nada disso exige que alguém pese o caminhão.
Plausível exige duas condições, e a segunda entrou pelo code review, o que é exatamente o ponto:
[floor(q), ceil(q)];Sem a segunda condição, um pedido de 39 kg em caixas de 20 kg (faixa [1,2]) aceitaria uma caixa de 20 kg e creditaria 39 kg, o que é pior que o bug original. Somado a isso, um teto absoluto: recebimento em kg nunca credita acima do pedido.
E aqui a decisão se conecta com a filosofia geral do sistema: aqui o sistema trava. A validação recusa um item de regime peso que não tenha peso médio em fonte nenhuma. Foi uma decisão tomada contra a recomendação do code review, a pedido explícito do dono, com o risco medido e mitigado: a validação testa o lote inteiro antes de mutar qualquer coisa, então nunca fecha um recebimento pela metade.
Recebimento é dinheiro entrando no estoque. Ali travar é barato, e deixar passar é caro e silencioso.
Cada uma segue a mesma estrutura: problema, alternativa óbvia, decisão, razão.
ProblemaTodo ERP pede que alguém mude o status manualmente. É trabalho extra que não produz nada, e é a primeira coisa que o usuário abandona quando está com pressa. Aí o dado apodrece.
Alternativa óbviaBotões de “marcar como separando”, “marcar como separado”.
DecisãoNenhum separador sabe que existe status. Ele toca em “iniciar separação”, o equivalente digital de tirar o papel do quadro e botar no bolso, e o resto é derivado dos atos reais: pesou, terminou, imprimiu.
“Sabe aquele negócio chato, que o usuário precisa mudar status só para dizer para o sistema que alguma coisa aconteceu? Não. Esses status vão ser reflexo da operação em tempo real, onde as outras pessoas nem sabem que existem esses status.”
O que mostraDado só é confiável quando registrá-lo é subproduto do trabalho, não trabalho adicional.
ProblemaQuatro balanças compartilhadas, cinco ou seis separadores. Sem identificação não há rastreabilidade. Com login por sessão, o separador digitaria usuário e senha toda vez que chegasse na balança, o que é inviável e ele abandonaria.
DecisãoNão é um caminho só. São dois, porque existem duas situações diferentes, e tratá-las igual foi o erro que o desenho evitou.
Caminho 1 · Pesar um pedido que já existeO pedido veio da atendente e o separador já iniciou a separação dele. O terminal fica sempre numa tela de espera que mostra os nomes de quem está separando naquele momento. O separador chega, toca no próprio nome, e a balança carrega os produtos do pedido dele. Sem PIN, sem senha, sem digitação.
Caminho 1 · toca no próprio nome
Caminho 2 · PIN
Isso funciona porque a tela não está autenticando ninguém, está dando continuidade a um processo que já tem dono. Tocar no nome de outra pessoa não abre nada de útil: apareceriam os produtos do pedido dela, não os do pedido que ele está carregando nas mãos. O caminho errado não leva a lugar nenhum, então não precisa ser trancado. O que ele garante é rastreabilidade: fica gravado quem pesou o quê.
Caminho 2 · Criar um pedido com o cliente na frenteAqui é venda presencial. O cliente está ali, e quem atende precisa criar o pedido, não continuar um. Isso é entrar no sistema de verdade, e é onde entra o PIN de 4 dígitos.
Pense nos dois lados do mesmo computador. De um lado, o uso tradicional: a pessoa digita o PIN e ganha acesso para criar o pedido e, se quiser, pesar por ali mesmo. Do outro lado, uma tela touch voltada para o galpão, onde basta tocar no nome para cair direto na pesagem. Mesma máquina, dois modos, escolhidos pelo que a pessoa precisa fazer.
E vale marcar a diferença: criar um pedido presencial não é a mesma coisa que a atendente criar um pedido. No presencial o cliente está na frente, o produto está ali, e o fluxo é mais curto porque a venda está acontecendo naquele instante.
A regra que torna isso seguroO PIN identifica, não autoriza. Quem se identifica opera com as permissões da estação, não com as do próprio perfil. Um conferente que se identifica numa balança não ganha acesso a preços por causa disso.
RazãoA fricção precisa cair para perto de zero numa ação que se repete dezenas de vezes por dia. A rastreabilidade não pode custar autenticação completa. E identidade nunca pode virar privilégio por acidente.
ProblemaO achado nº 2 da descoberta. Item esquecido é item invisível.
DecisãoA conferência passou a mostrar, na mesma linha, o que o cliente pediu e o que foi pesado. E a validação é granular: cada item recebe checkpoints de Produto, Variedade, Qualidade, Quantidade e Observação.

O refinamentoQuando o cliente estava fisicamente presente durante a separação e a pesagem, ele já viu a qualidade e as quantidades com os próprios olhos. Nesse caso a conferência granular pode ser pulada. A regra não é “sempre conferir tudo”, é “conferir o que ninguém ainda validou”. É o que separa design bom de design rígido.
Achado pós-lançamentoEsta é a melhor história do projeto, porque veio de usuários reais.
A conferente às vezes precisava devolver um pedido para a separação porque faltou pegar um item, não porque algo estava com defeito. Mas o único caminho de volta era “reportar um item”, que exige informar um motivo de defeito.
Então as conferentes começaram a inventar um defeito para conseguir devolver o pedido.
O estragoO sistema passou a acumular relatórios de qualidade falsos. O dado de “produto com problema” ficou contaminado por uma limitação de interface. Ninguém agia de má-fé: a interface simplesmente não oferecia o caminho honesto.
DecisãoCriar a Recoleta, um caminho de volta explícito que diz “faltou pegar, mas o produto existe”. Ela não reprova nada, preserva a conferência já feita e não conta como retorno de conferência.
“Inventar um defeito para conseguir devolver era exatamente o abuso que a recoleta veio eliminar.”
O que mostraUsuário burlando o sistema é diagnóstico de design, não indisciplina do usuário. E que eu continuo olhando depois do lançamento.
ProblemaO comprador quer ditar a lista de compras em vez de digitar. E os pedidos chegam como texto informal de WhatsApp: banana 5, abacaxi 2, tomate saladete 3cx.
Alternativa óbvia, e caraTranscrição de áudio por IA, interpretação por LLM. Em 2026, é o que praticamente todo mundo faria, e o que soa melhor numa demo.
Decisão, em duas partes
Entra · o texto como chegou no WhatsApp
Sai · cada item com a origem preservada
Extraído de: “couve 5 mç” em cada item: o parser guarda o trecho que originou a linha, então a atendente confere a interpretação sem precisar confiar nela.A razão, escrita na descoberta antes de eu saber se ia funcionar
“Se estiver bem escrito, apenas um código bem feito já consegue fazer esse trabalho e talvez mais assertivo do que uma inteligência artificial, porque a IA vai pegar muito contexto e vai até demorar mais.”
O detalhe que torna o parser confiávelEle sabe se abster. Tem níveis de confiança e correções aprendidas por cliente. O que ele não reconhece não vira palpite auto-preenchido, vira “não identificado” com uma sugestão de um clique. Um parser que erra em silêncio é pior que um parser que não tenta, porque a atendente para de conferir.
Por que é a decisão certa, e não economia de preguiçosoDeterminístico significa reproduzível, testável, corrigível, auditável e de custo marginal zero. Quatro propriedades que importam mais que “inteligente” quando o usuário está com pressa e precisa confiar no resultado. Uma atendente que descobre que o sistema “às vezes inventa um item” para de usar o campo, e aí você perde a feature inteira, não só a precisão dela.
ProblemaO sistema sabe o peso da carga e a capacidade do veículo. Quando um pedido não cabe, ele deve impedir?
DecisãoDepende de onde você está no fluxo, e a fronteira é explícita:

disabled aqui não evitaria um erro — pararia um caminhão.RazãoUm disabled na hora errada não previne um erro, ele para um caminhão. E quando o software impede algo que a pessoa sabe ser possível, ela perde a confiança no software inteiro, não só naquela tela.
ProblemaO desenho clássico separa “criar requisição” de “confirmar recebimento”. Mas no NR a mesma pessoa pode fazer as duas coisas, e a compra real quase nunca obedece à requisição: o comprador pede 20 caixas de banana de um fornecedor, chega lá e só 10 estão boas, então pega 10 de um e 10 de outro, cada um com preço e peso médio diferentes.
DecisãoUma requisição só, editável, viva. Quem criou entra de novo e edita. Quem está no CEASA abre a mesma requisição e confirma, altera, adiciona fornecedor ou remove produto.
“Por que eu vou fazer ele criar uma requisição e depois ter que acessar outro lugar só para confirmar aquela requisição?”
A folha que ia para o CEASA
A requisição, editável no celular
O desdobramentoA divergência vira informação. Confirmou 10 caixas e chegaram 8, a diferença fica registrada e rastreável. O sistema não esconde a diferença entre o planejado e o real, ele a torna visível.
ProblemaUm cliente chega no galpão, pega mercadoria, e o separador precisa registrar. Nem sempre dá para identificar na hora: pode ser alguém novo, pode estar corrido, pode não haver cadastro.
Alternativa óbviaObrigar a identificação, ou deixar o campo nulo.
DecisãoUma conta de trânsito, um destino explícito para a venda não identificada, que permite identificar depois. O caixa nunca trava por falta de cadastro, e o dado não vira um buraco.
RazãoObrigatoriedade em ponto de alta pressão gera dado lixo, porque o usuário escolhe qualquer cliente da lista para passar da tela. Campo nulo gera venda órfã. A terceira opção preserva as duas coisas.
ProblemaO sistema tinha dois números de pedido: um sequencial global e um “número diário” curto que reiniciava todo dia. Duas verdades sobre a mesma coisa.
DecisãoToda superfície voltada ao humano, do card da fila à busca, ao toast, à notificação, ao ticket impresso e ao board de entregas, passou a falar só o número global, que é o mesmo da nota fiscal. O número diário sumiu da interface inteira.
E um detalhe que só aparece em campo: o número impresso no ticket sai com zero à esquerda (042). Quem digitava 042 não achava o pedido 42. A busca passou a remover o zero à esquerda antes de casar.
RazãoDuas numerações significam que, em algum momento, duas pessoas ao telefone vão estar falando de pedidos diferentes achando que falam do mesmo.
Dois princípios irmãos:
Razão, com o caso concretoSe o estorno fosse um DELETE, uma perda registrada em 31/07 e desfeita em 03/08 sumiria do relatório de julho retroativamente, e o número que o gerente viu na semana passada nunca mais bateria. Com o ledger, julho continua contando +10 kg e agosto conta −10 kg. Os dois eventos são verdadeiros, em meses diferentes.
DecisãoO separador informa quantas caixas e qual modelo de caixa. Se usar pallet, escolhe qual. O sistema desconta tudo. A balança está conectada ao terminal, então o peso aparece no campo já com a tara descontada, e ele só confirma.
A decisão de hardware por trásOs pallets são de madeira e mudariam de peso conforme a umidade. Em vez de aceitar a imprecisão ou obrigar a pesar o pallet toda vez, a solução foi operacional: pesar cada pallet uma vez, etiquetar com número e peso, cadastrar no sistema. Validada com o dono, que confirmou que os pallets não ficam expostos.
O que mostraA solução de um problema de produto às vezes está fora da tela.
Fácil de esquecer num projeto cujas personas mais visíveis são mobile. Mas a atendente passa o dia inteiro em listas, com o WhatsApp aberto ao lado, e para ela o mouse é o gargalo. O sistema inteiro é operável sem tirar as mãos do teclado.
Navegação e ambiente
| Y | Abre a busca global. Digita duas letras do nome da página e navega direto, sem abrir o menu lateral. E ela só lista o que aquele perfil pode ver: a busca respeita as permissões, exatamente como a sidebar. |
| T | Alterna tema claro e escuro na hora. |
| B | Foca e já seleciona o campo de busca da tela. Selecionar junto importa: quem aperta B quase sempre quer buscar outra coisa, não continuar a busca anterior. |
| N | Cria um registro novo na tela em que você está. |
Filtros: a tecla é a primeira letra do filtroEste é o princípio que faz o resto não precisar ser decorado.
| D | Desativados. Vale em seis listas diferentes, sempre com o mesmo significado. |
| C | CEASA nos fornecedores, caixa nas embalagens. |
| W | WhatsApp. |
| P | Pallet nas embalagens, pendente nos produtos. |
| K / U | Modo de venda: kilo e unidade. |
Não é um mapa arbitrário que o usuário decora. É uma regra que ele deduz uma vez e aplica em qualquer tela.
Busca e acesso ao item
O detalhe que faz a diferençaA regra do ESC é sem estado. Não é “duplo ESC”. A função primária do ESC é tirar o foco; o ESC seguinte, já sem foco, cai naturalmente em “limpar”. O usuário aperta duas vezes e as duas coisas acontecem na ordem que ele espera, sem que ninguém tenha programado um contador.
E as guardas, que são metade do trabalhoNenhum atalho dispara quando o cursor está num campo de texto, numa área editável ou num select. E nenhum dispara com drawer ou modal aberto. Sem isso, um atalho de letra única transforma qualquer digitação em comando, que é a razão pela qual a maioria dos sistemas desiste de atalhos de letra única.
A decisão de não mostrar nadaOs atalhos não têm etiqueta visual na tela. Foi decisão deliberada do dono. Quem usa todo dia descobre e ganha velocidade; quem não usa não perde nada e não vê ruído numa interface que já é densa. Zero mudança de render.
O design system tem dois temas completos, com o mesmo conjunto de tokens. O que não é óbvio é qual veio primeiro.
O tema base é o escuro, “OLED Black × Electric Lime”, literalmente o cabeçalho do arquivo-fonte. O tema claro é um override. Isso inverte o default da indústria, e por um motivo operacional: as telas que mais rodam são as do galpão, em celular, muitas horas por dia, com brilho alto. Preto puro em tela OLED gasta menos bateria e cansa menos vista num ambiente com iluminação irregular.
O que isso custa, e onde o rigor apareceCor de status que fica vibrante e legível no preto frequentemente cai abaixo de 4.5:1 de contraste no branco. O arquivo de tokens trata isso explicitamente: há um conjunto de tokens de status só para texto sobre fundo claro, escurecidos o bastante para passar em AA, porque o valor vivo do tema escuro não passaria. Não é o mesmo token com opacidade diferente, são valores calculados para cada tema.
E por isso a regra de review visual do projeto tem quatro eixos, não dois: desktop e mobile, escuro e claro, mais navegação por teclado. Um bug de contraste só existe em uma das quatro combinações.
Tema claro · override
Tema escuro · base
O arcoO projeto começou como aplicação web, aberta no navegador. Terminou como um aparelho Android dedicado, que liga já travado no app e não faz mais nada além disso. Cada etapa desse caminho foi uma decisão, não uma consequência.
ProblemaUm aparelho compartilhado que também é um celular comum vira tudo menos ferramenta de trabalho: notificação, distração, aplicativo instalado por quem pegou antes, conta de outra pessoa ainda logada. E competir por atenção dentro da interface é uma briga que o software sempre perde.
O APK não embarca uma cópia do site. Ele carrega a interface ao vivo do servidor. A consequência prática é grande: toda melhoria de interface chega nos aparelhos no próximo deploy, sem gerar APK, sem pedir para ninguém atualizar nada, sem loja de aplicativo no meio.
O APK só precisa mudar quando muda a camada nativa: GPS em segundo plano, notificação, impressão, o kiosk. Ou seja, raramente. O que muda toda semana viaja pela web; o que muda uma vez por mês viaja pelo APK.
O app compara a versão instalada com a publicada pelo servidor e, se houver uma nova, mostra o aviso de atualização. Hoje está na décima versão.
E aqui vai uma limitação que eu prefiro contar do que esconder: fora da loja oficial, é impossível instalar automaticamente. O sistema operacional exige que a pessoa toque em “Atualizar”, autorize e confirme. Então o desenho assume isso em vez de brigar: o app não promete atualização invisível, ele avisa bem. Prometer o que a plataforma não permite é a forma mais rápida de construir uma feature que falha em silêncio.
Esta é a decisão da qual eu mais gosto nessa parte. Não existem duas versões do aplicativo, uma “normal” e uma “travada”. Existe um APK só, e ele detecta em tempo de execução se aquele aparelho foi provisionado como dispositivo dedicado da empresa.
Uma distribuição só, um build só, sem risco de instalar a versão errada no aparelho errado.
Aqui está o ponto em que a arquitetura obrigou a decisão. Como a interface vem da web ao vivo, um aparelho travado e sem internet não teria tela nenhuma para se destravar. Se o destravamento fosse uma tela do sistema, um celular sem sinal no fundo do galpão viraria um tijolo.
Por isso o caminho de saída é 100% nativo e offline: um gesto discreto num canto da tela abre a tela de liberação, e a senha mestre é verificada no próprio aparelho, sem rede. A trava também abre exceção para o serviço de impressão, porque o separador precisa imprimir o ticket mesmo com o celular travado.
EstadoImplementado, testado em aparelho real e em produção desde 11/07/2026.
Por que isso é diferencial de verdadeMuita empresa precisa que o celular seja uma ferramenta de trabalho e não um dispositivo pessoal, e quase sempre resolve isso com regra interna, aviso e confiança. Aqui a restrição é técnica: o aparelho literalmente não faz outra coisa. É design de produto operando na camada de hardware e configuração, não só de interface, e é o tipo de coisa que não aparece num protótipo.
O sistema tem design system próprio, gerado, e não um tema improvisado.
A regra de governança: os tokens vivem em um arquivo-fonte e são gerados para dentro da aplicação por script, que roda automaticamente antes de todo dev e de todo build. Ninguém edita o CSS da aplicação à mão. Ninguém escreve um hex dentro de um componente.
Os primitivos incluem Field (campo de formulário com label sempre associada), ClickableRow (linha de lista clicável, com semântica diferente para navegar e para abrir), Drawer, Modal, ConfirmDialog, EmptyState, SearchField, SortDropdown, SegmentedTabs, Toast, PageSkeleton, KpiCard e OfflineBanner.
O documento de padrões de UI abre com a tese dele:
Cada regra previne um bug que já aconteceu.
Não é um style guide aspiracional. É um registro de cicatrizes. Exemplos:
Field, porque htmlFor só funciona em elemento labelável, e um dropdown customizado precisa de aria-labelledby. Acessibilidade codificada no primitivo, não confiada à disciplina de quem escreve a tela.Por que isso importa: um design system que só padroniza aparência não paga o custo de existir. Este é um mecanismo de prevenção de erro. A regra existe porque o bug existiu, e está escrita no lugar onde a próxima pessoa vai tropeçar nela.
Stack: Next.js 16 com App Router e Server Actions, TypeScript, Supabase (Postgres, Auth e Realtime) e Vercel.
Banco em produção: 34 tabelas, 401 colunas, 58 funções Postgres, 20 triggers e 139 índices.
Cadastro real da empresa: 1.504 clientes, 1.548 endereços, 1.196 fornecedores, 382 produtos, e 72.992 linhas de histórico de vendas importadas e normalizadas do sistema legado.
Alcance da entrega, além do web app: autenticação Google OAuth com RBAC de 10 perfis; atualização em tempo real por broadcast; app Android próprio com modo dedicado (o aparelho liga travado no app), rastreamento de GPS em segundo plano e auto-atualização; geocodificação de endereços com mapa; notificações push; importação do sistema legado; e um diagnóstico de conformidade com a LGPD.
A suíte roda em 2,2 segundos, e isso é design, não sorte: ela cobre função pura. Toda regra de negócio que importa, do motor de conversão ao cálculo de baixa de estoque, da faixa plausível do recebimento à álgebra de custo dos dois regimes, do parser ao resolvedor de atalho de teclado, foi extraída para função pura e testada isoladamente.
Uma suíte que leva 2 segundos é uma suíte que você roda. Uma que leva 4 minutos é uma que você começa a pular.
regime-estoque, balanca-toledo, parse-preco, capacidade-veiculo, item-nao-separado. Cada um corresponde a uma decisão contada aqui em cima.A suíte não exercita Server Action, não sobe navegador e não testa sequência de interface. Toda a classe de defeito que é sequência, como estado que aparece na ordem errada, foco que vai para o lugar errado ou ramo que não renderiza, passa por ela intacta. É por isso que o review visual em quatro eixos existe como etapa formal. Saber onde a rede tem buraco vale mais que fingir que não tem.
Esta seção normalmente não existe em portfólio de design. Ela existe aqui porque um sistema que carrega documento de cliente, localização de entrega e o custo de compra de todo o catálogo tem um modelo de ameaça real.
O login é Google OAuth, e o caminho de senha foi desligado no servidor. Isso elimina de uma vez uma classe inteira de ataque: não há hash para vazar, não há reset para sequestrar, não há força bruta para limitar.
Toda rota passa por um middleware que resolve o usuário, encontra o funcionário e exige que ele esteja ativo antes de checar o nível de permissão da seção. Ativo é condição de entrada, não detalhe de cadastro: desativar um funcionário corta o acesso na hora, sem depender de expirar sessão.
O perfil Balança tem um caminho próprio, porque é uma estação e não uma pessoa: ele resolve quem se identificou e opera dentro das permissões da estação. É a regra da seção 7.2, implementada onde ela precisa estar.
Uma revisão de segurança encontrou uma Server Action que recebia a identidade de quem estava agindo como um parâmetro vindo do navegador. Havia uma verificação no banco que parecia proteger a posse do registro, mas o valor conferido era justamente o que o cliente podia alterar. Não autenticava nada.
A regra que ficou: Server Action que representa “quem fez a ação” deriva essa identidade da sessão no servidor, nunca de parâmetro do cliente. A pergunta a fazer em toda action nova é literal: “o cliente pode mentir esse valor?”
É o tipo de defeito que nenhum teste de tipo pega, porque o código está sintaticamente perfeito. Só uma leitura adversarial encontra.
As 34 tabelas têm segurança em nível de linha habilitada. E existem exatamente 5 políticas no banco inteiro.
Parece descuido. É o desenho. Uma tabela com a proteção ligada e nenhuma política nega tudo por padrão. Não existe “esqueci de proteger essa tabela”, porque o estado inicial de qualquer tabela nova já é fechado, e cada exceção precisou ser escrita à mão, uma por uma, com justificativa.
O acesso legítimo não passa por ali: ele passa pelo servidor, depois da checagem de perfil. As pouquíssimas políticas que existem cobrem os casos em que alguém de fora precisa ler exatamente a própria linha, e nada além dela.
A inversão vale a pena declarar: a maioria dos sistemas começa aberto e vai fechando conforme lembra. Este começa fechado e só abre onde alguém provou que precisa.
Existe um diagnóstico formal de conformidade, feito contra o schema real verificado, e não contra os arquivos locais, que são intenção e podem divergir. Ele inventaria o que é dado pessoal, identifica os operadores envolvidos e propõe a base legal de cada operação.
Do lado técnico: os dados ficam no Brasil, em São Paulo, o tráfego é HTTPS, o PIN é armazenado com hash, não há dado sensível clássico nem dado de cartão, e o acesso externo à base está fechado pelo modelo da seção anterior.
Construí o sistema sozinho, com IA como par de programação, em cerca de quatro meses. Vale ser específico sobre o que isso significa, porque “fiz com IA” hoje descreve coisas muito diferentes.
O que a IA não fez: ela não foi a campo. Não fez as entrevistas, não achou a conferência cega, não decidiu que o comprador não precisa de dashboard de estoque, não escolheu deixar a negociação fora do sistema, não descobriu que as conferentes estavam inventando defeitos. Essas são as decisões que fazem o produto ser esse produto, e todas vieram de observação e conversa.
O que a IA fez: implementação, refatoração e, o que eu acho mais valioso, revisão adversarial. Foi uma revisão automática de segurança que encontrou o bug de identidade da seção anterior.
A disciplina que eu tive que construir por cima. LLM produz texto plausível com altíssima fluência, e isso é perigoso de um jeito específico: crítica plausível e defeito real são indistinguíveis na leitura. Rodar review duas vezes no mesmo código devolve duas listas diferentes, e as duas parecem boas. A conclusão que virou regra no projeto: review por LLM é amostragem, não decisão.
Então achado não é resultado, achado é candidato. Ele só vira correção depois de passar por um portão:
A mesma disciplina vale para documentação, e é aí que ela é mais necessária, porque typecheck, lint e testes passam todos verdes com a documentação mentindo. Nada compila uma frase.
Ao montar o material deste projeto, uma auditoria da documentação contra o código achou um erro ainda aberto: a página de visão geral listava, como princípio de design, que o parser usava IA, e chegava a nomear o modelo. Não era verdade havia mais de um mês. Esse código existiu, nunca foi consumido por componente nenhum, e foi removido em 09/07/2026. Mais irônico ainda: a própria documentação já listava “a IA processa o pedido” como termo proibido em outra página.
O erro sobreviveu exatamente nas páginas de visão geral, que são as mais lidas e as menos auditadas, porque “não descrevem feature nenhuma”. Foram corrigidas no mesmo dia, com entrada no log de decisões.
É a melhor ilustração que eu tenho do ponto: a documentação apodrece em silêncio, e apodrece primeiro onde ninguém procura.
50 releases em produção entre 01/07 e 13/08/2026, ou seja 44 dias corridos, mais de um deploy por dia útil. Cada promoção recebe uma tag permanente com data e resumo, porque o histórico da branch de produção só mostra o que passou. A tag é o registro de o que foi ao ar e quando.
A estrutura: um tronco de testes onde o dono valida antes de qualquer coisa ir para produção; produção só avança para um commit já testado, nunca para código não verificado; e trabalho grande roda em ramo isolado, sem nunca tirar do ar o que o usuário está testando.
346 documentos markdown:
| Onde | O que é |
|---|---|
| Wiki de domínio 39 páginas | Entidades, fluxos, conceitos e personas. A fonte de verdade do negócio |
| Log de decisões 1.858 linhas, 73 entradas datadas | O que mudou, por quê, o que foi considerado e descartado, e o que ficou em aberto |
| Specs e planos | Especificação de design e plano de implementação, por feature |
| Reviews | Revisões, auditorias e follow-ups |
| Planejamento | Planos e specs de features maiores |
ai_texto_original / ai_processado são “fósseis de nome, não indicam uso de IA”. O rastro serve exatamente para isto: provar o que o sistema não faz.E há um processo formal de reconciliação da documentação contra o código: auditorias que verificam se a documentação ainda descreve o sistema real, com a regra explícita de que quando divergem, o código é a verdade técnica e a documentação é corrigida.
| O que prova | A evidência |
|---|---|
| Pesquisa de campo de verdade | Observação presencial recorrente, entrevista por função, transcrição de áudio, análise de artefato físico e dos 341 produtos do dado real |
| Enquadramento de problema, não coleta de requisito | “E se você faltar amanhã” mudou o escopo inteiro |
| Achar o problema que o cliente não sabia que tinha | A conferência cega |
| Saber onde o produto não deve entrar | A negociação com fornecedores ficou deliberadamente fora |
| Design de sistema, não de tela | 10 perfis, 3 esferas, 8 entidades, 7 fluxos ponta a ponta |
| Linguagem como material de design | 563 linhas definindo cada termo, incluindo os proibidos |
| Design system com governança | Token gerado de fonte única, e cada regra com proveniência de um bug real |
| Interface operável sem mouse | Busca global, filtros por primeira letra e acesso direto ao resultado, com as guardas que tornam atalho de letra única viável |
| Aprender com o comportamento pós-lançamento | A recoleta, com usuário burlando o sistema tratado como diagnóstico de design |
| Resistir à solução da moda | O parser determinístico, num ano em que tudo vira LLM |
| Entrega até a camada do aparelho | Aplicação web que virou celular dedicado, com um APK único que decide em tempo de execução se trava, e destravamento nativo que funciona sem rede |
| Segurança como decisão de produto | Negar por padrão no banco, e um PIN que identifica sem autorizar |
| Rastro de decisão | 1.858 linhas de log datado com o porquê de cada escolha |
A maioria dos cases de portfólio termina no protótipo. Este termina em software rodando, e continua, porque a operação real produz achados que nenhuma pesquisa prévia produziria.