[{"data":1,"prerenderedAt":1367},["ShallowReactive",2],{"home-projetos":3,"home-posts":1357,"paleta":1358},[4,474,613,796,924,1053],{"id":5,"title":6,"body":7,"contexto":437,"cor":438,"description":439,"destaque":440,"extension":441,"link":442,"meta":443,"navigation":206,"numeros":444,"ordem":104,"papel":457,"path":458,"periodo":459,"resumo":460,"seo":461,"stack":462,"stem":472,"__hash__":473},"projetos\u002Fprojetos\u002Fkravva.md","Kravva",{"type":8,"value":9,"toc":424},"minimark",[10,15,19,27,30,34,39,42,46,49,53,66,69,73,90,310,318,322,325,329,340,346,352,358,362,365,368,401,404,408,411,414,417,420],[11,12,14],"h2",{"id":13},"o-problema","O problema",[16,17,18],"p",{},"Queria um produto meu, com back-end de verdade. O Kravva é uma plataforma de palpites de futebol: o usuário palpita placares, entra em bolões, disputa rankings, acompanha jogos ao vivo e compra passes via PIX. Tem loja, afiliados e uma camada social.",[16,20,21,22,26],{},"Isso parece simples até o jogo começar. O placar muda e todo mundo quer ver na hora. A pontuação precisa ser recalculada no apito final. A API de dados esportivos tem cota diária. E o dinheiro entra por ",[23,24,25],"termo",{"id":25},"webhook",", que o provedor reenvia quando quer.",[16,28,29],{},"Eu era a única pessoa para resolver tudo isso, do produto ao deploy.",[11,31,33],{"id":32},"o-que-eu-fiz","O que eu fiz",[35,36,38],"h3",{"id":37},"back-end-modular","Back-end modular",[16,40,41],{},"A API é um monólito modular em NestJS 11 com PostgreSQL 16: 31 módulos, 82 entidades e 96 migrations. As migrations são SQL escrito à mão e a CI aplica todas num Postgres efêmero antes de qualquer deploy. Auth com JWT e refresh token, SSO Google e RBAC.",[35,43,45],{"id":44},"filas-para-tudo-que-não-cabe-numa-requisição","Filas para tudo que não cabe numa requisição",[16,47,48],{},"São 16 filas BullMQ: sincronização de partidas ao vivo, pontuação, notificações, e-mail e retenção de dados para a LGPD. Os jobs usam IDs determinísticos. Assim, o mesmo gol ou o mesmo apito final não é processado duas vezes.",[35,50,52],{"id":51},"tempo-real-em-cluster","Tempo real em cluster",[16,54,55,56,60,61,65],{},"A API roda ",[23,57,59],{"id":58},"cluster","em cluster"," Node, com mais de um worker. O Socket.io usa ",[23,62,64],{"id":63},"redis adapter","Redis adapter"," e autenticação JWT no handshake. Quando uma compra é confirmada ou o placar muda, o evento sai do worker que processou e chega ao socket do usuário, esteja ele em qual worker estiver.",[67,68],"viz-tempo-real",{},[35,70,72],{"id":71},"pix-com-webhook-e-ledger","PIX com webhook e ledger",[16,74,75,76,80,81,84,85,89],{},"O webhook chega com assinatura ",[23,77,79],{"id":78},"hmac","HMAC"," sobre o corpo cru, que valido com comparação em tempo constante. A confirmação roda numa transação com ",[23,82,83],{"id":83},"lock pessimista"," na linha do pagamento e grava num ",[23,86,88],{"id":87},"ledger","ledger financeiro append-only",". Estorno é lançamento novo, nunca update.",[91,92,97],"pre",{"className":93,"code":94,"language":95,"meta":96,"style":96},"language-ts shiki shiki-themes github-light github-dark-dimmed","\u002F\u002F Confirmação idempotente (pseudocódigo)\nawait db.transaction(async (tx) => {\n  const pagamento = await tx.selectForUpdate(pagamentos, { id }); \u002F\u002F trava a linha\n  if (pagamento.status === 'pago') return; \u002F\u002F reentrega do webhook: nada a fazer\n\n  pagamento.status = 'pago';\n  await tx.save(pagamento);\n  await ledger.registrarSeAusente(tx, pagamento); \u002F\u002F lançamento append-only\n  await acessos.concederSeAusente(tx, pagamento);\n  await comissao.creditar(tx, pagamento); \u002F\u002F só na primeira confirmação\n});\n\u002F\u002F O evento de tempo real só sai depois do commit\nemitirParaUsuario(pagamento.usuarioId, 'acesso_liberado');\n","ts","",[98,99,100,109,146,174,201,208,222,236,253,267,283,289,295],"code",{"__ignoreMap":96},[101,102,105],"span",{"class":103,"line":104},"line",1,[101,106,108],{"class":107},"sDN9O","\u002F\u002F Confirmação idempotente (pseudocódigo)\n",[101,110,112,116,120,124,127,130,133,137,140,143],{"class":103,"line":111},2,[101,113,115],{"class":114},"s7YZ4","await",[101,117,119],{"class":118},"sYgZi"," db.",[101,121,123],{"class":122},"sPO5f","transaction",[101,125,126],{"class":118},"(",[101,128,129],{"class":114},"async",[101,131,132],{"class":118}," (",[101,134,136],{"class":135},"stnAF","tx",[101,138,139],{"class":118},") ",[101,141,142],{"class":114},"=>",[101,144,145],{"class":118}," {\n",[101,147,149,152,156,159,162,165,168,171],{"class":103,"line":148},3,[101,150,151],{"class":114},"  const",[101,153,155],{"class":154},"sviXB"," pagamento",[101,157,158],{"class":114}," =",[101,160,161],{"class":114}," await",[101,163,164],{"class":118}," tx.",[101,166,167],{"class":122},"selectForUpdate",[101,169,170],{"class":118},"(pagamentos, { id }); ",[101,172,173],{"class":107},"\u002F\u002F trava a linha\n",[101,175,177,180,183,186,190,192,195,198],{"class":103,"line":176},4,[101,178,179],{"class":114},"  if",[101,181,182],{"class":118}," (pagamento.status ",[101,184,185],{"class":114},"===",[101,187,189],{"class":188},"s-HuK"," 'pago'",[101,191,139],{"class":118},[101,193,194],{"class":114},"return",[101,196,197],{"class":118},"; ",[101,199,200],{"class":107},"\u002F\u002F reentrega do webhook: nada a fazer\n",[101,202,204],{"class":103,"line":203},5,[101,205,207],{"emptyLinePlaceholder":206},true,"\n",[101,209,211,214,217,219],{"class":103,"line":210},6,[101,212,213],{"class":118},"  pagamento.status ",[101,215,216],{"class":114},"=",[101,218,189],{"class":188},[101,220,221],{"class":118},";\n",[101,223,225,228,230,233],{"class":103,"line":224},7,[101,226,227],{"class":114},"  await",[101,229,164],{"class":118},[101,231,232],{"class":122},"save",[101,234,235],{"class":118},"(pagamento);\n",[101,237,239,241,244,247,250],{"class":103,"line":238},8,[101,240,227],{"class":114},[101,242,243],{"class":118}," ledger.",[101,245,246],{"class":122},"registrarSeAusente",[101,248,249],{"class":118},"(tx, pagamento); ",[101,251,252],{"class":107},"\u002F\u002F lançamento append-only\n",[101,254,256,258,261,264],{"class":103,"line":255},9,[101,257,227],{"class":114},[101,259,260],{"class":118}," acessos.",[101,262,263],{"class":122},"concederSeAusente",[101,265,266],{"class":118},"(tx, pagamento);\n",[101,268,270,272,275,278,280],{"class":103,"line":269},10,[101,271,227],{"class":114},[101,273,274],{"class":118}," comissao.",[101,276,277],{"class":122},"creditar",[101,279,249],{"class":118},[101,281,282],{"class":107},"\u002F\u002F só na primeira confirmação\n",[101,284,286],{"class":103,"line":285},11,[101,287,288],{"class":118},"});\n",[101,290,292],{"class":103,"line":291},12,[101,293,294],{"class":107},"\u002F\u002F O evento de tempo real só sai depois do commit\n",[101,296,298,301,304,307],{"class":103,"line":297},13,[101,299,300],{"class":122},"emitirParaUsuario",[101,302,303],{"class":118},"(pagamento.usuarioId, ",[101,305,306],{"class":188},"'acesso_liberado'",[101,308,309],{"class":118},");\n",[16,311,312,313,317],{},"Se o webhook atrasa, o front consulta a transação. Depois de um tempo pendente, a API pergunta ao provedor e chama o mesmo caminho ",[23,314,316],{"id":315},"idempotencia","idempotente",".",[35,319,321],{"id":320},"operação","Operação",[16,323,324],{},"Rotinas em background precisam rodar em um único processo, então elegi um líder por lease no PostgreSQL. O deploy é automatizado com GitHub Actions e SSH numa VPS com Docker e Nginx. O front fica no Cloudflare Pages e os arquivos no R2. Há ambiente de produção e de homologação.",[11,326,328],{"id":327},"decisões-e-trade-offs","Decisões e trade-offs",[16,330,331,335,336,339],{},[332,333,334],"strong",{},"Idempotência em camadas no pagamento."," Histórico de eventos, ",[98,337,338],{},"SELECT ... FOR UPDATE",", checagem de estado e índices únicos como última barreira. Escolhi lock pessimista porque provedores de pagamento reenviam em rajada e a seção crítica é curta. O custo: entregas concorrentes do mesmo pagamento esperam umas pelas outras.",[16,341,342,345],{},[332,343,344],{},"BullMQ com jobId determinístico."," A fila garante entrega pelo menos uma vez, então o processamento precisa tolerar repetição. O ID do job impede duplicatas. O custo: a deduplicação só vale enquanto o job está retido. Já tive de pensar em incluir o placar no ID para que uma correção de VAR gerasse um job novo.",[16,347,348,351],{},[332,349,350],{},"SQL à mão nas migrations."," Dá controle sobre índices parciais e backfills, que o gerador do TypeORM faz mal. O custo: nada garante que a entidade continue igual à coluna real. Isso depende de disciplina e de teste.",[16,353,354,357],{},[332,355,356],{},"Cluster em vez de orquestrador."," Dois workers aproveitam a máquina sem a complexidade de Kubernetes. O custo é visível no próximo ponto: estado em memória por processo.",[11,359,361],{"id":360},"resultado","Resultado",[16,363,364],{},"O Kravva está em produção, com deploy automatizado, testes unitários com Jest, testes de integração com Postgres real e CI que faz dry-run das migrations. Não divulgo o número de usuários.",[16,366,367],{},"Três incidentes reais viraram correção:",[369,370,371,386,392],"ul",{},[372,373,374,377,378,381,382,385],"li",{},[332,375,376],{},"WebSocket caindo no cluster."," O front recebia \"Session ID unknown\" (HTTP 400). A causa: o Nginx não repassava o upgrade de WebSocket, o cliente caía para ",[23,379,380],{"id":380},"long polling"," e as requisições do mesmo cliente chegavam a workers diferentes. Adicionei o bloco do Socket.io no Nginx com ",[98,383,384],{},"Upgrade",", e o erro sumiu. Foi um problema separado do Redis adapter, que adicionei no mesmo ajuste para resolver uma falha silenciosa: um evento emitido num worker não chegava a sockets conectados em outro.",[372,387,388,391],{},[332,389,390],{},"Cota da API de futebol estourada na Copa."," Passei a usar um snapshot ao vivo compartilhado entre workers e um rate limiter global no Redis.",[372,393,394,397,398,317],{},[332,395,396],{},"PWA servindo arquivos velhos após deploy."," Tratei a recuperação de chunk e o ",[98,399,400],{},"skipWaiting",[16,402,403],{},"Sobre IA: boa parte do código foi escrita com apoio de Cursor e Claude. As decisões, a revisão e a operação são minhas, e sei explicar cada uma delas.",[11,405,407],{"id":406},"o-que-eu-faria-diferente","O que eu faria diferente",[16,409,410],{},"Começaria com observabilidade: logs estruturados, métricas das filas e alertas. Essa é uma lacuna que conheço.",[16,412,413],{},"Quebraria o serviço de sincronização de jogos, que cresceu demais.",[16,415,416],{},"Fecharia o fallback de long polling com WebSocket puro ou sticky sessions. Sem isso, o polling continua frágil com mais de um worker.",[16,418,419],{},"Testaria as migrations contra um dump anonimizado, não só contra um banco vazio. E padronizaria o acesso a dados, que hoje mistura repositórios próprios com o repositório padrão do TypeORM.",[421,422,423],"style",{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":96,"searchDepth":111,"depth":148,"links":425},[426,427,434,435,436],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":428},[429,430,431,432,433],{"id":37,"depth":148,"text":38},{"id":44,"depth":148,"text":45},{"id":51,"depth":148,"text":52},{"id":71,"depth":148,"text":72},{"id":320,"depth":148,"text":321},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"Produto próprio","oklch(0.7 0.17 150)","Plataforma de palpites de futebol que construí e opero sozinho: NestJS, filas, tempo real em cluster e PIX idempotente.","Back-end completo em produção, operado por uma pessoa só","md","https:\u002F\u002Fkravva.com.br",{},[445,448,451,454],{"valor":446,"rotulo":447},"16","filas BullMQ",{"valor":449,"rotulo":450},"31","módulos no back-end",{"valor":452,"rotulo":453},"96","migrations",{"valor":455,"rotulo":456},"2","ambientes: produção e homologação","Fundador, produto e engenharia","\u002Fprojetos\u002Fkravva","2026 · em produção","Plataforma de palpites de futebol com rankings, bolões, loja com PIX e placar ao vivo. Construo e opero sozinho, do produto à infraestrutura.",{"title":6,"description":439},[463,464,465,466,467,468,469,470,471],"Vue 3","Quasar","NestJS","PostgreSQL","Redis","BullMQ","Socket.io","Docker","GitHub Actions","projetos\u002Fkravva","PL5tIm8fdOuruEkpYXlXFC9yDooGzEYgFgQ7oZSOObY",{"id":475,"title":476,"body":477,"contexto":581,"cor":582,"description":583,"destaque":584,"extension":441,"link":585,"meta":586,"navigation":206,"numeros":587,"ordem":111,"papel":599,"path":600,"periodo":601,"resumo":602,"seo":603,"stack":604,"stem":611,"__hash__":612},"projetos\u002Fprojetos\u002Fonboarding-erp.md","Onboarding digital de um ERP financeiro",{"type":8,"value":478,"toc":569},[479,481,484,487,489,493,496,503,506,510,513,517,520,524,530,532,538,544,553,555,558,561,563,566],[11,480,14],{"id":13},[16,482,483],{},"Num ERP financeiro B2B, o onboarding é o primeiro contato do cliente com o produto. Se ele é longo ou confuso, o cliente desiste antes de usar o sistema.",[16,485,486],{},"O onboarding antigo era uma SPA em Vue 3 que tinha crescido por acúmulo. A jornada se organizava em macroestados com subetapas: contratos, lojas, valores, assinatura e parametrização. O que aparecia mudava conforme o contrato e o status do lead. Parte das entradas ainda passava pelas telas MVC do ERP. Era difícil saber em que ponto o cliente estava e mais difícil ainda mudar o fluxo sem quebrar outro caminho.",[11,488,33],{"id":32},[35,490,492],{"id":491},"um-front-novo-do-zero","Um front novo, do zero",[16,494,495],{},"Comecei o repositório e construí a SPA nova com Vue 3.5, TypeScript, Vite e Pinia, organizada por features. Cada etapa tem a sua página, o seu estado e o seu serviço. Componentes, validação e o cliente HTTP ficam numa camada compartilhada.",[16,497,498,499,502],{},"O onboarding virou uma jornada linear de ",[332,500,501],{},"oito etapas",": contato, código de confirmação, usuários, empresas, confirmação cadastral, proposta, termos e conclusão.",[504,505],"viz-onboarding",{},[35,507,509],{"id":508},"o-back-end-como-fonte-da-verdade","O back-end como fonte da verdade",[16,511,512],{},"O front valida para orientar a pessoa, mas quem decide é o servidor. Sessão, elegibilidade e progresso ficam no back-end, e a interface só avança quando ele confirma. Isso evita um problema clássico de wizard: o navegador achar que a etapa terminou enquanto o servidor discorda.",[35,514,516],{"id":515},"uma-segunda-jornada-sem-duplicar-o-front","Uma segunda jornada sem duplicar o front",[16,518,519],{},"Depois veio a autocontratação: contato, produtos, resumo, comercial e sucesso. Ela tem regras de navegação e dados diferentes do onboarding. Coloquei as duas no mesmo repositório com routers, stores e builds separados por jornada, e extraí os componentes visuais comuns para uma camada compartilhada. Um checkpoint local permite retomar o rascunho, mas catálogo, preço e conclusão continuam confirmados pelo servidor.",[35,521,523],{"id":522},"back-end-em-net","Back-end em .NET",[16,525,526,527,529],{},"Também trabalho no back-end em C# e .NET com MongoDB. Ajustei os casos de uso de aceite de termos e de sincronização de empresas, com testes. Tornei atômica e ",[23,528,316],{"id":315}," a geração do link de onboarding, com nova tentativa só no conflito de transação previsto.",[11,531,328],{"id":327},[16,533,534,537],{},[332,535,536],{},"Oito etapas explícitas em vez de estados implícitos."," Cada etapa tem entrada e saída claras, o que facilita medir o funil e mudar uma parte sem mexer no resto. O custo: uma jornada linear é mais rígida, e cada exceção de negócio precisa virar uma regra explícita de navegação.",[16,539,540,543],{},[332,541,542],{},"Isolar as jornadas e compartilhar só a apresentação."," Duplicar o front inteiro seria mais rápido no primeiro dia e caro em todos os outros. Juntar tudo num router só acoplaria regras que mudam por motivos diferentes. Separar estado e navegação e compartilhar componentes é o meio-termo. O custo é disciplina: decidir caso a caso o que é comum de verdade.",[16,545,546,552],{},[332,547,548,549,317],{},"Migrar por entrada, não por ",[23,550,551],{"id":551},"big bang"," As entradas antigas foram redirecionadas para a experiência nova aos poucos. O legado continua de pé enquanto houver consumidor.",[11,554,361],{"id":360},[16,556,557],{},"O novo onboarding substitui o antigo nas entradas migradas. O tempo médio de onboarding caiu de 12 para 3–4 minutos, e a conversão até virar cliente subiu 20%.",[16,559,560],{},"A autocontratação ganhou uma jornada própria sem duplicar a base de código.",[11,562,407],{"id":406},[16,564,565],{},"Instrumentaria o funil por etapa desde o primeiro commit, com eventos padronizados, para conseguir dizer qual mudança pesou mais no ganho de tempo.",[16,567,568],{},"Também escreveria testes de contrato entre o front e o back para as transições de etapa. Os testes unitários cobrem componentes, stores e guards, mas o ponto mais frágil de um wizard é o front e o back discordarem sobre em que etapa a pessoa está.",{"title":96,"searchDepth":111,"depth":148,"links":570},[571,572,578,579,580],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":573},[574,575,576,577],{"id":491,"depth":148,"text":492},{"id":508,"depth":148,"text":509},{"id":515,"depth":148,"text":516},{"id":522,"depth":148,"text":523},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"Consultoria PJ","oklch(0.68 0.15 250)","Construí do zero o novo front de onboarding de um ERP financeiro em Vue 3 e TypeScript: oito etapas claras, autocontratação no mesmo repositório e back-end em .NET.","12 → 3–4 min de onboarding",null,{},[588,591,593,596],{"valor":589,"rotulo":590},"8","etapas no novo onboarding",{"valor":455,"rotulo":592},"jornadas no mesmo front, isoladas",{"valor":594,"rotulo":595},"12 → 3–4 min","tempo de onboarding",{"valor":597,"rotulo":598},"+20%","conversão até virar cliente","Desenvolvedor full stack, autor de mais de 90% dos commits do front novo","\u002Fprojetos\u002Fonboarding-erp","2026","Construí do zero o novo front de onboarding do ERP, em Vue 3 e TypeScript, e depois a autocontratação no mesmo repositório. O onboarding caiu de 12 para 3–4 minutos.",{"title":476,"description":583},[463,605,606,607,608,609,610,471],"TypeScript","Vite","Pinia","C#",".NET","MongoDB","projetos\u002Fonboarding-erp","w-ahApkdmmVO41zhAN_LIJ2o9Bjwjk_EtbmWo9ZYgLU",{"id":614,"title":615,"body":616,"contexto":581,"cor":771,"description":772,"destaque":773,"extension":441,"link":585,"meta":774,"navigation":206,"numeros":775,"ordem":148,"papel":788,"path":789,"periodo":601,"resumo":790,"seo":791,"stack":792,"stem":794,"__hash__":795},"projetos\u002Fprojetos\u002Fpainel-erp.md","Painel administrativo do ERP",{"type":8,"value":617,"toc":759},[618,620,623,626,637,639,643,646,660,663,667,674,678,681,685,693,711,714,716,722,728,734,740,742,745,748,750,753,756],[11,619,14],{"id":13},[16,621,622],{},"O ERP financeiro é um monorepo grande, com várias gerações convivendo. Tem um MVC histórico em .NET Framework, APIs e hosts em .NET moderno, um console de processamento, bibliotecas compartilhadas e, dentro disso tudo, uma SPA administrativa em Vue 3. Muita gente commita ali ao mesmo tempo: foram mais de 3 mil commits no período em que trabalhei.",[16,624,625],{},"Três problemas apareciam juntos:",[369,627,628,631,634],{},[372,629,630],{},"Telas importantes ainda viviam no MVC, mas não dava para desligar o legado de uma vez: clientes, rotinas e regras dependiam dele.",[372,632,633],{},"Áreas administrativas novas precisavam nascer na SPA, conversando com serviços que outros fronts também consomem.",[372,635,636],{},"O ciclo de desenvolvimento era lento, e ninguém sabia dizer em qual etapa o tempo ia embora.",[11,638,33],{"id":32},[35,640,642],{"id":641},"migração-por-partes-não-por-big-bang","Migração por partes, não por big bang",[16,644,645],{},"Levei uma área do MVC para a SPA sem quebrar quem ainda usava o caminho antigo. A mudança atravessou as camadas:",[369,647,648,651,654,657],{},[372,649,650],{},"redirecionamento no MVC;",[372,652,653],{},"rotas, menu e tipos na SPA;",[372,655,656],{},"gates de migração na camada de negócio compartilhada;",[372,658,659],{},"rotinas compatíveis no back-end em .NET, com testes.",[16,661,662],{},"O legado continua de pé enquanto houver consumidor. Cada superfície migra quando está pronta.",[35,664,666],{"id":665},"gestão-de-contratos-e-termos","Gestão de contratos e termos",[16,668,669,670,673],{},"Construí a área administrativa para consultar e gerir contratos e o estado de aceite dos termos: telas, entrada no menu, integração com a API e mocks para testar. A capacidade foi liberada por ",[23,671,672],{"id":672},"feature flag",". A SPA compõe a experiência; quem decide e persiste é o back-end, o mesmo que atende o onboarding.",[35,675,677],{"id":676},"um-shell-com-variantes-de-navegação","Um shell com variantes de navegação",[16,679,680],{},"Para não duplicar menus e layout a cada variante da SPA, separei três coisas: a escolha da variante, o contrato de navegação (o que cada variante oferece) e um shell visual comum. Assim, uma variante nova da SPA vira configuração, sem copiar o layout inteiro.",[35,682,684],{"id":683},"medir-antes-de-otimizar","Medir antes de otimizar",[16,686,687,688,692],{},"Antes de mexer em qualquer configuração, criei ",[23,689,691],{"id":690},"benchmark","benchmarks"," reproduzíveis de três jornadas: subir o ambiente e abrir o app no navegador, gerar o build e rodar os testes. Com os números na mão:",[369,694,695,698,701,704],{},[372,696,697],{},"separei a validação do empacotamento;",[372,699,700],{},"deixei o build sequencial como opção;",[372,702,703],{},"otimizei a configuração do Vite e passei a carregar alguns recursos só quando são usados;",[372,705,706,707,317],{},"montei um CI experimental com jobs paralelos e testes divididos em ",[23,708,710],{"id":709},"shard","shards",[712,713],"viz-build",{},[11,715,328],{"id":327},[16,717,718,721],{},[332,719,720],{},"Medir jornadas reais, não o tempo percebido."," \"O build está lento\" não diz o que fazer. Benchmark com mesma máquina, mesmos comandos e mediana de várias rodadas diz. O custo é tempo gasto montando o harness antes da primeira otimização.",[16,723,724,727],{},[332,725,726],{},"CI paralelo encurta a espera, mas não é de graça."," Jobs independentes terminam antes, só que cada um paga o próprio setup. A espera de quem abriu o PR cai, e os minutos totais de runner podem subir. Por isso o CI ficou como experimento, documentado com esse custo, e não substituiu o pipeline oficial.",[16,729,730,733],{},[332,731,732],{},"Feature flag e migração por superfície."," Liberar por etapas reduz o risco, mas deixa dois caminhos vivos por um tempo. Esse custo precisa de dono e de data para acabar.",[16,735,736,739],{},[332,737,738],{},"Shell compartilhado com contrato de navegação."," Centralizar evita menus duplicados. O custo é que mudar o contrato exige revisar todas as variantes.",[11,741,361],{"id":360},[16,743,744],{},"Em benchmark local, o build da SPA caiu de 223 s para 130 s, e o build da SPA com a biblioteca de componentes caiu de 246 s para 175 s. A jornada de desenvolvimento no navegador passou de cerca de 70 s para cerca de 38 s, na mediana. São medições locais com amostra pequena, e a proposta ainda está em validação com o time. Não é ganho de produção.",[16,746,747],{},"A área migrada e a gestão de contratos e termos estão na SPA, com testes, convivendo com o MVC enquanto ele for necessário.",[11,749,407],{"id":406},[16,751,752],{},"Faria cada métrica opcional do benchmark entrar explicitamente no critério de sucesso. Hoje, uma métrica habilitada pode piorar sem reprovar a execução.",[16,754,755],{},"Faria o filtro de caminhos do workflow cobrir também as mudanças no host MVC, que podem afetar a SPA.",[16,757,758],{},"Antes de propor o CI paralelo como padrão, coletaria uma amostra maior e pareada, comparando a espera e os minutos de runner no mesmo período.",{"title":96,"searchDepth":111,"depth":148,"links":760},[761,762,768,769,770],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":763},[764,765,766,767],{"id":641,"depth":148,"text":642},{"id":665,"depth":148,"text":666},{"id":676,"depth":148,"text":677},{"id":683,"depth":148,"text":684},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"oklch(0.72 0.14 185)","SPA Vue 3 dentro de um monorepo .NET com MVC legado: migração de telas sem big bang, gestão de contratos e termos, e um build medido antes de ser otimizado.","Build de 223 s para 130 s, medido antes de otimizar",{},[776,779,782,785],{"valor":777,"rotulo":778},"218","commits meus na SPA (~23% do período)",{"valor":780,"rotulo":781},"223 → 130 s","build da SPA, em benchmark local",{"valor":783,"rotulo":784},"~70 → ~38 s","jornada de dev no navegador (mediana)",{"valor":786,"rotulo":787},"68","arquivos de teste tocados","Desenvolvedor full stack, um dos principais contribuidores da SPA","\u002Fprojetos\u002Fpainel-erp","Uma SPA Vue 3 que convive com um MVC legado dentro de um monorepo .NET. Migrei telas por partes, entreguei a gestão de contratos e termos e medi o build antes de otimizá-lo.",{"title":615,"description":772},[463,605,606,607,793,608,609,471],"Element Plus","projetos\u002Fpainel-erp","kZWn-oxLWldfp2onp8MH_j1SXn8Zy6IG-6BbSO8sVAo",{"id":797,"title":798,"body":799,"contexto":899,"cor":900,"description":901,"destaque":902,"extension":441,"link":585,"meta":903,"navigation":206,"numeros":904,"ordem":176,"papel":914,"path":915,"periodo":916,"resumo":917,"seo":918,"stack":919,"stem":922,"__hash__":923},"projetos\u002Fprojetos\u002Fligafest.md","Ingresso em grupo da LigaFest",{"type":8,"value":800,"toc":888},[801,803,806,809,812,814,818,821,825,828,839,842,845,849,852,854,860,866,868,871,874,876,879,882,885],[11,802,14],{"id":13},[16,804,805],{},"A LigaFest é um evento gratuito de card games, realizado num shopping, com mais de 8 mil participantes inscritos. Na primeira edição, a entrada lotou.",[16,807,808],{},"O motivo era simples. Cada pessoa precisava do próprio QR code. Um grupo de cinco levava cinco QR codes até a fila. Muita gente do shopping chegava curiosa, sem cadastro nenhum. O negócio queria zero burocracia, mas alguém precisava controlar o acesso e liberar as pulseiras.",[16,810,811],{},"A solução foi iniciativa minha. Eu fazia o front-end completo da LigaFest e vi o problema na primeira edição. Para a edição seguinte, propus o ingresso em grupo.",[11,813,33],{"id":32},[35,815,817],{"id":816},"ingresso-em-grupo","Ingresso em grupo",[16,819,820],{},"Um responsável, o titular, inscreve a família ou os amigos. Um único QR code libera todos. Cada titular pode convidar até 5 dependentes.",[35,822,824],{"id":823},"dados-mínimos-por-desenho","Dados mínimos, por desenho",[16,826,827],{},"Estudei LGPD e propus um fluxo baseado em minimização de dados:",[369,829,830,833,836],{},[372,831,832],{},"O ingresso é recuperado só com o e-mail. Sem senha, sem perfil completo.",[372,834,835],{},"A tela do ingresso mostra o QR code e o e-mail, sem o nome da pessoa.",[372,837,838],{},"Os dependentes ficam anonimizados. Guardamos só idade e parentesco.",[16,840,841],{},"Com isso, as pulseiras saem na entrada sem que todo mundo precise se cadastrar.",[843,844],"viz-ingresso-grupo",{},[35,846,848],{"id":847},"um-qr-code-no-local","Um QR code no local",[16,850,851],{},"Para quem chegava sem cadastro, deixei um QR code impresso na entrada. Ele levava direto ao campo de e-mail. A pessoa fazia o cadastro rápido ou recuperava os ingressos ali mesmo, sem procurar mensagem na caixa de entrada.",[11,853,328],{"id":327},[16,855,856,859],{},[332,857,858],{},"Recuperar o ingresso só com e-mail."," Reduz o atrito: não há senha para esquecer, nem conta para criar. Também reduz o que fica guardado. O custo é de segurança. Quem sabe o e-mail de alguém consegue tentar recuperar o ingresso, e o QR code pode ser compartilhado. Num evento gratuito o risco por ingresso é menor, mas o custo existe e eu sabia dele. Num produto com mais em jogo, endureceria isso.",[16,861,862,865],{},[332,863,864],{},"Dependentes anonimizados."," Eu não precisava do nome de uma criança para liberar uma pulseira. Precisava saber que existe um dependente e qual a faixa etária e o parentesco. O custo é operacional: sem nome, não dá para identificar um dependente específico. Para o evento, isso bastava.",[11,867,361],{"id":360},[16,869,870],{},"A aglomeração na entrada caiu muito. Um QR code passou a liberar o grupo inteiro, e a LigaFest chegou a mais de 8 mil participantes inscritos com o mínimo de dados pessoais armazenados. Isso também reduz o impacto em caso de incidente: o que não se guarda não vaza.",[16,872,873],{},"Preciso ser preciso aqui. O cadastro do titular ainda coletava dados completos: nome, CPF, data de nascimento, telefone, CEP e gênero. Isso era requisito do evento. A minimização vale para três pontos: a recuperação do ingresso (só e-mail), a exibição (sem nome) e os dependentes (só idade e parentesco). O sistema não coletava o mínimo absoluto, e eu não vou contar como se coletasse.",[11,875,407],{"id":406},[16,877,878],{},"Aplicaria a mesma lógica ao titular. Se o evento só precisa liberar a entrada, a pergunta honesta é: precisamos mesmo de CPF e CEP? A evolução natural é coletar só o necessário também dele.",[16,880,881],{},"Endureceria a recuperação por e-mail. Um link com expiração, limite de tentativas por e-mail e IP, QR code assinado e de uso único e invalidação no check-in reduziriam o risco de enumeração e de compartilhamento. Essas são as melhorias que eu proporia, e não as descrevo como já feitas.",[16,883,884],{},"Definiria por escrito a base legal do tratamento, com consentimento ou legítimo interesse, e a diferença entre anonimização e pseudonimização para cada dado dos dependentes. Eu estudei LGPD para desenhar o fluxo, mas esse é um ponto em que o ideal é trabalhar junto com jurídico, e não decidir sozinho no código.",[16,886,887],{},"Por fim, mediria o efeito. Eu vi a fila diminuir, mas não tenho números de tempo de entrada antes e depois, e por isso não publico nenhum.",{"title":96,"searchDepth":111,"depth":148,"links":889},[890,891,896,897,898],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":892},[893,894,895],{"id":816,"depth":148,"text":817},{"id":823,"depth":148,"text":824},{"id":847,"depth":148,"text":848},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"LigaMagic · CLT","oklch(0.8 0.15 80)","Ingresso em grupo com um único QR code e dados mínimos (LGPD) para a LigaFest, evento gratuito com mais de 8 mil inscritos.","1 QR code libera o grupo inteiro",{},[905,908,911],{"valor":906,"rotulo":907},"+8 mil","participantes inscritos",{"valor":909,"rotulo":910},"1","QR code por grupo",{"valor":912,"rotulo":913},"até 5","dependentes por ingresso","Desenvolvedor front-end (front completo e iniciativa de produto)","\u002Fprojetos\u002Fligafest","2023–2025","Por iniciativa própria, criei o ingresso em grupo da LigaFest: um QR code libera o titular e até 5 dependentes anonimizados, com foco em LGPD.",{"title":798,"description":901},[920,463,605,607,921],"Nuxt 3","Tailwind CSS","projetos\u002Fligafest","zGrThoHHPGB8KjOPnNb_HCATQv6rz22Ufuu6vPNUM7A",{"id":925,"title":926,"body":927,"contexto":899,"cor":1027,"description":1028,"destaque":1029,"extension":441,"link":585,"meta":1030,"navigation":206,"numeros":1031,"ordem":203,"papel":1044,"path":1045,"periodo":916,"resumo":1046,"seo":1047,"stack":1048,"stem":1051,"__hash__":1052},"projetos\u002Fprojetos\u002Fcircuitos-ligamagic.md","Circuitos LigaMagic e LigaPokémon",{"type":8,"value":928,"toc":1016},[929,931,934,937,940,943,945,949,952,956,964,967,971,974,976,982,988,994,996,999,1002,1005,1007,1010,1013],[11,930,14],{"id":13},[16,932,933],{},"A LigaMagic é o maior marketplace brasileiro de card games: Magic, Pokémon, Yu-Gi-Oh!, One Piece e outros. Os Circuitos são as ligas de torneios organizadas nas lojas de todo o país, com inscrição, rodadas e ranking.",[16,935,936],{},"Quando assumi o front-end, ele estava no início e mal estruturado. Já tinha usuários de verdade: na temporada 2024, os circuitos da LigaMagic e da LigaPokémon somaram mais de 2.400 torneios, em mais de 150 lojas, com mais de 6.000 jogadores. E nos dias de evento, os apps atendem mais de 2.000 jogadores durante os torneios, em eventos com mais de 12.000 pessoas.",[16,938,939],{},"Um front frágil nessas condições não é só dívida técnica. Quando algo quebra num dia de evento, há gente esperando na loja.",[16,941,942],{},"Havia ainda um segundo desafio: os circuitos derivados de cada card game tinham marcas diferentes. O produto precisava parecer um circuito de Magic num lugar e um de Pokémon em outro, sem duas bases de código.",[11,944,33],{"id":32},[35,946,948],{"id":947},"refatoração-da-base-herdada","Refatoração da base herdada",[16,950,951],{},"Refatorei praticamente toda a base de front que recebi, em Nuxt 3 e Vue 3 com TypeScript. A exceção foi o módulo de cadastro de lojas, onde mexi em partes: refatorei trechos do fluxo de inscrição e de pagamento das lojas, sem assumir o módulo inteiro.",[35,953,955],{"id":954},"theming-multi-marca","Theming multi-marca",[16,957,958,959,963],{},"Para as marcas dos circuitos, usei Tailwind com variáveis CSS. As cores e os demais ",[23,960,962],{"id":961},"design tokens","tokens"," de cada marca ficam em variáveis. Os componentes não conhecem a marca, só os tokens. Trocar de circuito é trocar os valores, e não o código.",[965,966],"viz-theming",{},[35,968,970],{"id":969},"referência-técnica-do-front","Referência técnica do front",[16,972,973],{},"Eu definia padrões e decisões de front e participava de UI\u002FUX, brainstorming e refinamento com o time de produto. Era uma referência técnica do front, sem cargo formal de liderança.",[11,975,328],{"id":327},[16,977,978,981],{},[332,979,980],{},"Refatorar quase tudo em vez de remendar."," Uma base mal estruturada, usada por milhares de jogadores, pedia uma estrutura em que eu pudesse mudar sem medo. O custo é alto: refatorar uma base em uso consome tempo que não vira funcionalidade nova, e cada mudança grande pede cuidado para não quebrar o que já funcionava. Fiz porque a alternativa era empilhar mudanças sobre um terreno instável.",[16,983,984,987],{},[332,985,986],{},"Variáveis CSS em vez de uma folha de estilos por marca."," Com uma folha por marca, cada ajuste visual seria feito várias vezes e as marcas se afastariam com o tempo. Com variáveis, o ajuste é feito uma vez, e uma marca nova vira um conjunto de valores. O custo: o sistema de tokens precisa ser bem pensado desde o início. Se um componente embute uma cor fixa, ele quebra o tema.",[16,989,990,993],{},[332,991,992],{},"Não assumir o módulo de cadastro de lojas por inteiro."," Mexi nele em partes, como o fluxo de inscrição e pagamento, mas ele não foi refatorado por mim de ponta a ponta. O custo é que algumas partes do front seguiram a organização anterior, fora do padrão que defini no resto.",[11,995,361],{"id":360},[16,997,998],{},"O front sustentou a temporada de 2024 e os dias de evento, com os números acima. Esses números descrevem a escala do produto, e não um ganho que eu atribua só ao meu trabalho.",[16,1000,1001],{},"Não tenho métricas de antes e depois para a refatoração, como quantidade de bugs ou tempo de entrega, e não vou inventar. O que posso dizer: as marcas derivadas passaram a ser tratadas como tema, e não como cópia, e o time passou a ter padrões definidos para trabalhar.",[16,1003,1004],{},"O trabalho foi feito durante o período em que estive na LigaMagic, em regime CLT.",[11,1006,407],{"id":406},[16,1008,1009],{},"Mediria desde o início. Se eu tivesse registrado taxa de erros e tempo de entrega antes de refatorar, hoje poderia mostrar o efeito em números.",[16,1011,1012],{},"Combinaria a refatoração com testes de interface nos fluxos críticos, como inscrição e pagamento, antes de mexer neles. Refatorar sem essa rede de segurança exige mais cuidado manual do que deveria.",[16,1014,1015],{},"Documentaria o sistema de tokens do theming logo no começo. A flexibilidade das variáveis só vale se todo mundo no time souber quais tokens existem e quando usar cada um.",{"title":96,"searchDepth":111,"depth":148,"links":1017},[1018,1019,1024,1025,1026],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":1020},[1021,1022,1023],{"id":947,"depth":148,"text":948},{"id":954,"depth":148,"text":955},{"id":969,"depth":148,"text":970},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"oklch(0.65 0.18 295)","Front-end dos circuitos de torneios da LigaMagic: refatoração de uma base herdada e theming multi-marca com variáveis CSS.","+2.400 torneios em +150 lojas, num front que herdei",{},[1032,1035,1038,1041],{"valor":1033,"rotulo":1034},"+2.400","torneios na temporada 2024",{"valor":1036,"rotulo":1037},"+150","lojas",{"valor":1039,"rotulo":1040},"+6.000","jogadores na temporada",{"valor":1042,"rotulo":1043},"2.000+","jogadores nos apps em dias de evento","Responsável pelo front-end e referência técnica","\u002Fprojetos\u002Fcircuitos-ligamagic","Fui responsável pelo front dos circuitos de torneios da LigaMagic e da LigaPokémon: +2.400 torneios, +150 lojas e +6.000 jogadores na temporada 2024.",{"title":926,"description":1028},[920,463,605,607,921,1049,1050],"PrimeVue","Shadcn-Vue","projetos\u002Fcircuitos-ligamagic","rhKr_MWQFF7imXaCTHFRt64p7eXXofNI3crzw4DFe6c",{"id":1054,"title":1055,"body":1056,"contexto":1332,"cor":1333,"description":1334,"destaque":1335,"extension":441,"link":585,"meta":1336,"navigation":206,"numeros":1337,"ordem":210,"papel":1344,"path":1345,"periodo":1346,"resumo":1347,"seo":1348,"stack":1349,"stem":1355,"__hash__":1356},"projetos\u002Fprojetos\u002Fblippy.md","Blippy",{"type":8,"value":1057,"toc":1319},[1058,1060,1063,1066,1069,1071,1075,1078,1081,1085,1088,1091,1232,1236,1239,1243,1246,1250,1257,1259,1269,1275,1281,1287,1289,1292,1295,1298,1300,1303,1310,1313,1316],[11,1059,14],{"id":13},[16,1061,1062],{},"Queria estudar de perto o que sustenta um chat em tempo real: entregar a mensagem rápido, não perdê-la quando algo cai e não duplicá-la quando o cliente tenta de novo. O Blippy é uma plataforma de comunidades no estilo Discord, com servidores, canais de texto e voz, permissões, anexos e compartilhamento de tela.",[16,1064,1065],{},"Os problemas difíceis estão no meio do caminho. Se a API grava a mensagem no banco e morre antes de avisar o broker, ninguém recebe. Se publica antes de gravar, anuncia algo que não existe. Se o cliente reenvia um POST que deu timeout, a mensagem aparece duas vezes.",[16,1067,1068],{},"Desenhei o contrato pensando em escala grande, mas implementei só o que um MVP pede, em fatias verificáveis.",[11,1070,33],{"id":32},[35,1072,1074],{"id":1073},"dois-planos","Dois planos",[16,1076,1077],{},"Separei o sistema em control plane e data plane. A API em NestJS (Fastify), com PostgreSQL e Redis, decide e persiste. Um gateway de tempo real em Go mantém os WebSockets e empurra os eventos. NATS JetStream em cluster liga os dois. Se a API cai, o sistema degrada. Se o gateway cai, o tempo real para. Há uma checagem de CI que falha se o gateway importar um driver de banco.",[16,1079,1080],{},"O gateway em Go foi escrito com forte apoio de IA. O contrato Protobuf, as decisões e a revisão são minhas.",[35,1082,1084],{"id":1083},"envio-idempotente-e-outbox","Envio idempotente e outbox",[16,1086,1087],{},"O cliente gera um nonce e mostra a mensagem como pendente. A API grava mensagem, nonce e evento de saída (outbox) na mesma transação. Só depois publica no NATS. Se o processo morrer entre o commit e a publicação, um reconciliador republica.",[1089,1090],"viz-chat",{},[91,1092,1094],{"className":93,"code":1093,"language":95,"meta":96,"style":96},"\u002F\u002F Dentro da transação de envio (pseudocódigo)\ntry {\n  await tx.query(\n    `INSERT INTO message_nonces (channel_id, author_id, nonce, message_id)\n     VALUES ($1, $2, $3, $4)`, [canal, autor, nonce, id]);\n} catch (err) {\n  \u002F\u002F Retry do mesmo envio: devolve o id que já existe\n  if (isUniqueViolation(err)) return { duplicate: true, id: await buscarPorNonce(...) };\n  throw err;\n}\nawait tx.query('INSERT INTO messages ...');\nawait tx.query('INSERT INTO outbox ...'); \u002F\u002F evento na mesma transação\n",[98,1095,1096,1101,1108,1120,1125,1133,1144,1149,1185,1193,1198,1213],{"__ignoreMap":96},[101,1097,1098],{"class":103,"line":104},[101,1099,1100],{"class":107},"\u002F\u002F Dentro da transação de envio (pseudocódigo)\n",[101,1102,1103,1106],{"class":103,"line":111},[101,1104,1105],{"class":114},"try",[101,1107,145],{"class":118},[101,1109,1110,1112,1114,1117],{"class":103,"line":148},[101,1111,227],{"class":114},[101,1113,164],{"class":118},[101,1115,1116],{"class":122},"query",[101,1118,1119],{"class":118},"(\n",[101,1121,1122],{"class":103,"line":176},[101,1123,1124],{"class":188},"    `INSERT INTO message_nonces (channel_id, author_id, nonce, message_id)\n",[101,1126,1127,1130],{"class":103,"line":203},[101,1128,1129],{"class":188},"     VALUES ($1, $2, $3, $4)`",[101,1131,1132],{"class":118},", [canal, autor, nonce, id]);\n",[101,1134,1135,1138,1141],{"class":103,"line":210},[101,1136,1137],{"class":118},"} ",[101,1139,1140],{"class":114},"catch",[101,1142,1143],{"class":118}," (err) {\n",[101,1145,1146],{"class":103,"line":224},[101,1147,1148],{"class":107},"  \u002F\u002F Retry do mesmo envio: devolve o id que já existe\n",[101,1150,1151,1153,1155,1158,1161,1163,1166,1169,1172,1174,1177,1179,1182],{"class":103,"line":238},[101,1152,179],{"class":114},[101,1154,132],{"class":118},[101,1156,1157],{"class":122},"isUniqueViolation",[101,1159,1160],{"class":118},"(err)) ",[101,1162,194],{"class":114},[101,1164,1165],{"class":118}," { duplicate: ",[101,1167,1168],{"class":154},"true",[101,1170,1171],{"class":118},", id: ",[101,1173,115],{"class":114},[101,1175,1176],{"class":122}," buscarPorNonce",[101,1178,126],{"class":118},[101,1180,1181],{"class":114},"...",[101,1183,1184],{"class":118},") };\n",[101,1186,1187,1190],{"class":103,"line":255},[101,1188,1189],{"class":114},"  throw",[101,1191,1192],{"class":118}," err;\n",[101,1194,1195],{"class":103,"line":269},[101,1196,1197],{"class":118},"}\n",[101,1199,1200,1202,1204,1206,1208,1211],{"class":103,"line":285},[101,1201,115],{"class":114},[101,1203,164],{"class":118},[101,1205,1116],{"class":122},[101,1207,126],{"class":118},[101,1209,1210],{"class":188},"'INSERT INTO messages ...'",[101,1212,309],{"class":118},[101,1214,1215,1217,1219,1221,1223,1226,1229],{"class":103,"line":291},[101,1216,115],{"class":114},[101,1218,164],{"class":118},[101,1220,1116],{"class":122},[101,1222,126],{"class":118},[101,1224,1225],{"class":188},"'INSERT INTO outbox ...'",[101,1227,1228],{"class":118},"); ",[101,1230,1231],{"class":107},"\u002F\u002F evento na mesma transação\n",[35,1233,1235],{"id":1234},"contrato-único-e-ids","Contrato único e IDs",[16,1237,1238],{},"O contrato entre TypeScript e Go é Protobuf gerado, com checagem na CI. Os IDs são Snowflake, com lease do node-id no Redis. As tabelas de mensagens são particionadas no Postgres.",[35,1240,1242],{"id":1241},"voz-tela-e-desktop","Voz, tela e desktop",[16,1244,1245],{},"Voz e tela usam LiveKit, um SFU self-hosted. O app desktop é Electron, com pipeline de release multiplataforma. Os recursos de mídia avançada, como supressão de ruído e múltiplas lives, são de um colaborador do projeto.",[35,1247,1249],{"id":1248},"qualidade","Qualidade",[16,1251,1252,1253,1256],{},"Gates com semgrep, gitleaks, actions fixadas por SHA e ",[23,1254,1255],{"id":1255},"mutation testing",". O outbox tem teste com injeção de falha: a API \"morre\" depois do commit e o teste confere que o reconciliador entrega o evento.",[11,1258,328],{"id":327},[16,1260,1261,1264,1265,1268],{},[332,1262,1263],{},"NATS com JetStream em vez de Kafka, RabbitMQ ou Redis Pub\u002FSub."," Os subjects hierárquicos com curinga, como ",[98,1266,1267],{},"guild.*.channel.*.msg",", casam com o roteamento por servidor e canal. Kafka pesa para tópicos tão granulares. Redis Pub\u002FSub não persiste nada. O custo: hoje o gateway assina o modo simples do NATS e ninguém consome o stream persistido, que só acumula mensagens. É preparação para consumidores futuros, e precisa de limite de retenção.",[16,1270,1271,1274],{},[332,1272,1273],{},"Outbox transacional em vez de publicar e torcer."," Garante que nenhum evento se perde. O custo é aceitar entrega pelo menos uma vez, com possível duplicata. O consumidor precisa tolerar isso.",[16,1276,1277,1280],{},[332,1278,1279],{},"Postgres particionado em vez de Scylla."," O modelo de dados segue o formato do Scylla, com chave por canal e janela de tempo, mas roda em Postgres. Evita custo operacional prematuro e deixa o caminho de migração aberto. O custo: o schema é criado em código, no boot, sem migrations versionadas.",[16,1282,1283,1286],{},[332,1284,1285],{},"Payload opaco no gateway."," O gateway numera os eventos sem decodificar o conteúdo. Fica mais barato por mensagem e eventos novos não exigem mudar o gateway. O custo é perder visibilidade do conteúdo naquela camada.",[11,1288,361],{"id":360},[16,1290,1291],{},"O Blippy roda de ponta a ponta: servidores, canais de texto e voz, anexos, compartilhamento de tela e app desktop. Sou o autor principal, com cerca de 94% dos commits. O restante é, em sua maior parte, de um colaborador, nos recursos de mídia.",[16,1293,1294],{},"Teve incidente real: o LiveKit chegou a anunciar a porta ICE errada, e corrigi.",[16,1296,1297],{},"Não há números de usuários, e não vou inventá-los. O valor aqui está no desenho e nas garantias que consegui testar.",[11,1299,407],{"id":406},[16,1301,1302],{},"Usaria migrations versionadas desde o começo. Schema criado no boot não altera nada e ainda pode colidir quando várias réplicas sobem juntas.",[16,1304,1305,1306,1309],{},"Colocaria ",[98,1307,1308],{},"FOR UPDATE SKIP LOCKED"," no reconciliador do outbox. Com mais de uma réplica da API, todas varreriam as mesmas linhas e publicariam duplicado.",[16,1311,1312],{},"Resolveria o resume de sessão fora da memória do processo. Hoje, depois de um deploy, o cliente refaz a sessão do zero.",[16,1314,1315],{},"Trocaria o mutex único do gateway por mapas particionados e definiria retenção para o stream do JetStream e para a tabela de nonces.",[421,1317,1318],{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":96,"searchDepth":111,"depth":148,"links":1320},[1321,1322,1329,1330,1331],{"id":13,"depth":111,"text":14},{"id":32,"depth":111,"text":33,"children":1323},[1324,1325,1326,1327,1328],{"id":1073,"depth":148,"text":1074},{"id":1083,"depth":148,"text":1084},{"id":1234,"depth":148,"text":1235},{"id":1241,"depth":148,"text":1242},{"id":1248,"depth":148,"text":1249},{"id":327,"depth":111,"text":328},{"id":360,"depth":111,"text":361},{"id":406,"depth":111,"text":407},"Projeto pessoal","oklch(0.78 0.12 210)","Plataforma de comunidades em tempo real, estilo Discord: API NestJS, gateway em Go, NATS JetStream, outbox transacional e voz com LiveKit.","Mensagens em tempo real sem perder nem duplicar",{},[1338,1341],{"valor":1339,"rotulo":1340},"3","nós no cluster NATS JetStream",{"valor":1342,"rotulo":1343},"~94%","dos commits, como autor principal","Autor principal (arquitetura e implementação)","\u002Fprojetos\u002Fblippy","2026 · em desenvolvimento","Plataforma de comunidades com servidores, canais de texto e voz, anexos e tela. Sou o autor principal; o projeto tem um colaborador nos recursos de mídia avançada.",{"title":1055,"description":1334},[465,466,467,1350,1351,1352,1353,463,1354],"Go","NATS JetStream","Protobuf","LiveKit","Electron","projetos\u002Fblippy","F73k892LR5sWNm4mU9lfKAz37AtBfUAYmAu09SdyOdA",[],{"projetos":1359,"posts":1366},[1360,1361,1362,1363,1364,1365],{"path":458,"title":6,"contexto":437},{"path":600,"title":476,"contexto":581},{"path":789,"title":615,"contexto":581},{"path":915,"title":798,"contexto":899},{"path":1045,"title":926,"contexto":899},{"path":1345,"title":1055,"contexto":1332},[],1791910169305]