Inventário técnico

Débitos técnicos

Tudo o que hoje custa tempo, dinheiro ou risco no AZ Collections 3.0 — o que é cada problema, por que ele dói, onde afeta e como se resolve.

  • Projeto AZ Collections 3.0
  • Levantado em 21/08/2026
  • Revisado em 25/09/2026
  • Itens 13
  • Base leitura do código dos 5 repositórios

O que é débito técnico

Débito técnico é o custo futuro de uma decisão tomada hoje para ir mais rápido. Nem sempre é erro: às vezes é a escolha certa — entregar antes, decidir depois. O problema não é contrair a dívida, é perder a conta dela.

A metáfora é a de um empréstimo, e ela é literal em dois aspectos. Primeiro, existe juros: cada dia com o atalho no lugar, todo trabalho que passa por perto fica um pouco mais caro. Um serviço com 2.000 linhas não atrasa uma tarefa só — atrasa todas as que o tocarem, para sempre, até alguém quebrá-lo. Segundo, existe vencimento: alguns débitos são cobrados de uma vez, sem aviso. Um servidor apertado não cobra nada enquanto o movimento é pequeno — e cobra tudo junto no dia em que deixa de ser.

Por isso este documento não é uma lista de reclamações, e sim um balanço. Cada item traz o que é, o dano concreto que causa, onde ele aparece e o caminho de saída. O que se decide depois é ordem de pagamento — e essa decisão é de negócio, não de engenharia.

O que este documento não é

Não é uma crítica a quem escreveu o código. A maior parte dos itens abaixo é resultado de decisões corretas para o momento em que foram tomadas — um sistema em reconstrução acumula dívida por definição. O valor de escrever tudo é justamente parar de pagar juros sem saber.

O balanço

13 Itens mapeados

Todos com evidência medida no código, não estimada.

5 Altos

Cobram juros todo dia — ou cobram de uma vez, quando o movimento aperta.

7 Médios

Encarecem o trabalho ou deixam uma promessa sem cumprir. Outro 1 é baixo.

2 Novos nesta revisão

Os dois do sorteio: sinais de risco e resultado presos à conta.

Os 13 itens, na ordem em que aparecem neste documento.
# Débito Área Severidade
01Três arquiteturas convivendo no coreBackendAlto
02Serviços-monstro de até 2.276 linhasBackendAlto
03Suíte de testes que não sabe reprovarBackendMédio
04Dashboard ainda lê do banco de produçãoDashboardAlto
05Dashboard defasada do 3.0DashboardMédio
06Quatro serviços numa máquina só, com pool no padrãoInfraestruturaAlto
07Nenhum serviço tem redundânciaInfraestruturaMédio
08Pouca visibilidade de erros no aplicativoAplicativoMédio
09Funcionalidades desligadas sem data de voltaAplicativoBaixo
10runtimeVersion parado em 3.0.0 enquanto o app está na 3.0.4AplicativoAlto
11Quatro versões de Prisma em quatro serviçosConsistênciaMédio
12Sinais de risco do sorteio nunca são anuladosBackendMédio
13Apagar uma conta apagaria bilhetes e resultados do sorteioBackendMédio

Backend principal

3 itens
01

Três arquiteturas convivendo no mesmo servidor

Alto api.collectionsaz.com · src/
O que é

O servidor principal tem cerca de 89.700 linhas de código, sem contar os testes, organizadas de três formas diferentes ao mesmo tempo, sem fronteira garantida entre elas:

as três organizações, lado a lado em src/ (dev, 24/09/2026, sem testes)1. Camadas (o legado) src/controllers/ 64 arquivos src/services/ 115 arquivos src/db/repositories/ 86 arquivos 2. DDD por contexto (o 3.0) — 9 contextos src/v3/ identity · collection · feed · finance · interests notifications · raffle · search · social 3. Módulo isolado (nem um nem outro) src/modules/shipping/ e ainda: src/mechanisms/ src/share/ src/workers/

A regra oficial do projeto é clara — funcionalidade nova nasce em src/v3/ no padrão DDD, código legado só recebe correção no lugar. Ela já foi escrita: o docs/arquitetura.md do core traz o ADR-001 (monólito modular com DDD-lite), de 28/06/2026 — mas com status Proposta, nunca aprovado. O problema não é a regra: é que nada no código a torna obrigatória. Quem chega não distingue as três zonas, e as ferramentas não impedem que uma invada a outra.

O problema

Toda tarefa começa com a mesma pergunta perdida: onde isso deveria morar? E como a resposta depende de quem está fazendo, o mesmo tipo de regra acaba escrito em dois lugares — uma vez no serviço antigo, outra no caso de uso novo. Quando divergem, divergem em silêncio: as duas versões continuam funcionando, dando respostas diferentes.

O custo não aparece numa tarefa específica. Ele aparece como lentidão constante em todas, e como uma classe de bug difícil de rastrear — o comportamento muda conforme o caminho que a requisição tomou.

Onde afeta

Em toda funcionalidade nova que toca o servidor principal, e com força maior nas áreas onde 2.0 e 3.0 se encostam: loja, checkout, pedidos e perfil. Também afeta a entrada de qualquer desenvolvedor novo no projeto, que precisa aprender três convenções antes de escrever a primeira linha.

A solução

Não é reescrever o servidor. Reescrita grande de sistema em produção costuma custar o dobro do previsto e entregar o mesmo comportamento — com bugs novos. O caminho é tornar a fronteira explícita e deixar o legado encolher sozinho:

  1. Tirar o ADR-001 do status de proposta: revisar, aprovar e acrescentar exemplos dos dois lados. O documento já existe — falta virar decisão.
  2. Marcar o legado como zona congelada — um README em cada pasta antiga dizendo o que pode e o que não pode acontecer ali.
  3. Ligar uma verificação automática que impeça src/v3/ de importar do legado. É o que transforma a regra em garantia.
  4. Absorver src/modules/shipping/ num dos dois lados, eliminando o terceiro padrão.
  5. A partir daí, cada funcionalidade nova nasce certa, e o legado só diminui.
Considerações

Cobre a fronteira inteira: a aprovação do documento de decisão, a marcação das zonas, a verificação automática e a absorção do módulo solto. Não resolve o legado que já existe — isso é o item 02 —, mas estanca o crescimento da dívida, que é o mais urgente dos dois.

02

Serviços-monstro: até 2.276 linhas num arquivo só

Alto api.collectionsaz.com · src/services, src/db/repositories
O que é

Cinco arquivos concentram as regras mais importantes do negócio — venda, troca, checkout e conta — e cada um cresceu muito além do que se lê de uma vez:

os maiores arquivos do servidor principal (dev, 24/09/2026)2.276 src/services/store-service.ts loja, anúncios, etiquetas 1.570 src/db/repositories/item-repository.ts 1.559 src/services/trade-service.ts trocas 1.524 src/services/checkout-service.ts pagamento 1.020 src/services/user-service.ts conta 900 src/services/auth-service.ts desde 21/08: store +220 · item +109 · trade +176 · checkout +298
O problema

Arquivo desse tamanho não cabe na cabeça de ninguém de uma vez. Na prática isso produz três efeitos, todos caros:

  • Mudança arriscada. Alterar uma linha do checkout exige entender 1.524 para ter certeza de que nada mais dependia dela. Como ninguém faz isso toda vez, a certeza vira torcida.
  • Teste difícil. Para testar um comportamento pequeno é preciso montar todo o resto do serviço. É por isso que a cobertura fica baixa justamente onde o dinheiro passa.
  • Conflito constante. Dois desenvolvedores mexendo em coisas não relacionadas colidem no mesmo arquivo.
Onde afeta

Diretamente no coração comercial: venda, troca, pagamento e envio. São exatamente as áreas em que um bug custa dinheiro de verdade — e as que mais recebem alteração, porque são as que o negócio mais mexe.

A solução

Quebrar por caso de uso, um de cada vez, e só quando houver motivo para tocar no arquivo. Refatoração em bloco de código que ninguém pediu para mudar é risco sem retorno.

  1. Antes de mexer, escrever testes que descrevem o comportamento atual — inclusive o esquisito. Eles são a rede: se algum quebrar depois, a extração mudou o que não devia.
  2. Extrair um caso de uso por vez (“comprar etiqueta”, “calcular frete”), com o serviço antigo passando a apenas chamar o novo.
  3. Quando o serviço antigo virar só uma lista de chamadas, apagá-lo.

Essa é a parte do trabalho em que a IA mais ajuda: mapear todos os pontos de uso e gerar os testes de caracterização é lento à mão e rápido com assistência.

Considerações

O maior item da lista, e também o mais diluível: não precisa virar projeto próprio. Cada vez que uma tarefa encostar num desses arquivos, ela paga a extração daquele pedaço. Assim o custo se dissolve nas entregas e o risco de cada passo fica pequeno.

03

Uma suíte de testes que não sabe reprovar

Médio api.collectionsaz.com · configuração do Jest
O que é

O servidor principal tem 145 arquivos de teste, mas rodar a suíte sempre termina em erro — mesmo que todos os testes passassem. A causa é uma exigência de cobertura mínima de 90% contra cerca de 15% reais: o Jest considera isso uma falha e encerra sinalizando erro. E eles não passam todos: o docs/ESTADO.md do core registra 11 falhas pré-existentes (trade-service, pagarme-service, notify-email-verification), escondidas atrás do erro de sempre.

jest.config.js do corecollectCoverage: true ← fixo: toda rodada instrumenta o repo inteiro coverageThreshold.global: 90 ← lines · branches · functions · statements real: ~15%

Há ainda um efeito prático pior: com a cobertura ligada de forma fixa, a rodada completa leva perto de 5 minutos e trava a máquina de desenvolvimento. Por isso ela está proibida desde 24/08 — a verificação do dia a dia é o tsc --noEmit mais o teste do arquivo tocado. Ninguém roda a suíte inteira.

O problema

Um teste só vale enquanto alguém confia nele — e enquanto alguém o roda. Como o resultado é sempre “erro” e a rodada completa não pode acontecer, um teste que quebrasse fora do arquivo tocado passaria despercebido. As 11 falhas registradas são a prova: estão lá, conhecidas, e nada obriga a consertá-las.

É também o motivo de não existir portão automático nas mudanças: não dá para pedir que a esteira barre código quebrado quando a suíte reprova sempre.

Onde afeta

Em toda alteração do servidor principal. É o débito que encarece todos os outros: sem rede de testes confiável, quebrar os serviços-monstro do item 02 fica bem mais arriscado do que precisaria ser.

A solução
  1. Separar as duas coisas: desligar o collectCoverage fixo, para que um comando rode os testes e responda só sobre eles; outro, separado, mede cobertura.
  2. Corrigir as 11 falhas pré-existentes, para a suíte voltar a sair limpa.
  3. Ajustar a exigência de cobertura para o número real de hoje, travando o que existe.
  4. Subir esse número aos poucos, conforme as extrações do item 02 forem trazendo testes novos.
  5. Com o resultado voltando a ser confiável, ligar o portão na esteira.
Considerações

Quase toda mudança de configuração, não de código — o melhor retorno de toda a lista. As 11 falhas são o único trabalho de verdade. Destrava a confiança na suíte inteira, permite rodá-la de novo e, com ela, dá a segurança para atacar o item 02.

Dashboard administrativa

2 itens
04

A dashboard ainda lê do banco de produção

Alto az-dash.collectionsaz.com · az-dash-api
O que é

A dashboard não faz consultas leves: são relatórios com contagens e agregações escritos à mão. O código já está pronto para mandá-los a uma réplica de leitura — o client prismaReplica existe —, mas ele tem um fallback deliberado: sem DATABASE_REPLICA_URL, cai na DATABASE_URL. E o ambiente Production do GitHub não tem essa variável. Em produção, portanto, toda leitura “da réplica” continua indo para o mesmo banco que atende o aplicativo.

az-dash-api/src na main (25/09/2026)69 consultas SQL escritas à mão ($queryRawUnsafe), em 23 arquivos 14 contagens (.count) prismaReplica.ts DATABASE_REPLICA_URL ?? DATABASE_URL env Production DATABASE_URL · sem DATABASE_REPLICA_URL → a “réplica” é o primário escrita pelo core categorias · produtos · sorteio escrita direta auth · usuários · bloqueios · comentários · curtidas seguidores · grupos · cidades · países categorias do usuário · Pagar.me (11 módulos)
O problema

Relatório e venda disputam a mesma máquina. Quando alguém do time abre um painel pesado, o banco divide memória e disco entre a consulta administrativa e o checkout de um colecionador — e o colecionador é quem espera.

O sintoma é traiçoeiro porque aparece longe da causa: o aplicativo fica lento em um horário específico e ninguém liga o fato a alguém consultando um relatório. E a sensação de que o problema “já foi resolvido”, porque o código fala em réplica, torna o sintoma ainda mais difícil de ligar à causa.

Onde afeta

Na experiência de todo colecionador que usa o aplicativo enquanto o time consulta a dashboard. O risco cresce com a base: quanto mais dados, mais cara cada agregação, e hoje não há teto — uma consulta pesada pode segurar o banco sem limite de tempo.

A solução

A parte de código está quase toda feita; o que falta é, sobretudo, infraestrutura:

  1. Um teto de tempo já. Um limite por consulta no usuário do banco da dashboard impede que qualquer relatório trave a produção enquanto a réplica não existe. Duas linhas de configuração.
  2. Medir. Ligar a medição de consultas do PostgreSQL e descobrir quais das 69 realmente doem — lentidão de painel quase sempre é índice faltando.
  3. Subir a réplica física de produção e cadastrar DATABASE_REPLICA_URL no ambiente Production. Nenhuma linha de código muda: o prismaReplica passa a apontar para ela sozinho.
  4. Terminar de mover as escritas para o core, como já foi feito com categorias, produtos e sorteio. Enquanto os outros 11 módulos escreverem direto, a dashboard continua precisando de acesso de escrita ao banco do produto.
Considerações

O teto de tempo é barato e protege desde já. A réplica é o único passo que depende de máquina nova — e, com o código pronto, é também o que encerra o assunto: vale medir antes para dimensioná-la, não para decidir se ela vem.

05

A dashboard não enxerga o mundo 3.0

Médio az-dash.collectionsaz.com · prisma/schema.prisma
O que é

A dashboard modela só metade das tabelas do sistema, e ainda lê uma que o 3.0 aposentou:

cobertura da dashboard sobre o banco do produtoservidor principal 92 tabelas dashboard 47 tabelas (51%) + 3 do próprio painel (az_dash) ───────────────── não modeladas 45 tabelas ainda lê negotiation → aposentada; proposta e troca vivem no chat (productService devolve essas linhas como “proposals”) contorna AZ Play por SQL cru · sorteio pelo core não enxerga propostas do chat (banco próprio) · fila de moderação
O problema

Parte do que o 3.0 criou já ganhou tela — o AZ Play, por consultas escritas à mão, e o sorteio, pelo core. O resto não tem: se uma proposta de troca precisar ser conferida ou uma moderação precisar de revisão, não há por onde — só consultando o banco à mão. E a tela de produto ainda mostra como propostas as linhas da tabela aposentada, que já não recebem nada novo.

E administrar pelo banco é o pior dos mundos: sem registro de quem fez, sem validação e com risco de escrever no lugar errado.

Onde afeta

No atendimento e na moderação. Com o 3.0 nas lojas, o conteúdo 3.0 já é o volume principal — e o débito cresce sozinho a cada funcionalidade nova do core que nasce sem contrapartida no painel.

A solução
  1. Regenerar a descrição do banco a partir do servidor principal, trazendo as 45 tabelas ausentes — o que também tira o AZ Play do SQL escrito à mão.
  2. Remover o que ficou órfão, como a leitura da tabela de negociação substituída pelo chat.
  3. Listar, com o time de atendimento, quais telas ainda faltam — provavelmente poucas: propostas (lidas do chat, que é o dono) e a fila de moderação.
  4. Construir só essas.
Considerações

Pôr a descrição do banco em dia é a parte barata, e já elimina o risco de administrar uma tabela morta. As telas novas vêm depois — e só as que o atendimento apontar como necessárias.

Infraestrutura e produção

2 itens
06

Quatro serviços numa máquina só — e o core com o pool de conexões no padrão

Alto box az-prod · 2 vCPU · 8 GB · DB_POOL_SIZE
O que é

O que o plano de subida previa já aconteceu: o serviço de mídia roda em Docker, o chat 3.0 está em produção, e mídia e chat têm endereço próprio (az-media.collectionsaz.com, chat.collectionsaz.com). Mas o endereço é próprio, a máquina não: as esteiras de produção dos quatro backends apontam para o mesmo executor, na mesma box de 2 núcleos e 8 GB.

o que roda na az-prod, com o teto de memória de cada umservidor principal 1 GB (+ 512 MB do worker de e-mail) dashboard ~512 MB chat 3.0 512 MB chat legado 512 MB (de pé em 26/08, conferido no PM2) serviço de mídia 3 GB · 1,5 núcleo (contêiner) ─────── teto somado ~6 GB, em 8 GB de máquina moderação automática desligada em produção (MODERATION_ENABLED=false) DB_POOL_SIZE do core ausente no ambiente Production → pool padrão de 5
O problema

São dois gargalos. O primeiro é processamento: o serviço de mídia analisa cada imagem e vídeo com modelos de inteligência artificial que rodam na própria máquina, e um único vídeo passa por várias análises em sequência. É por isso que a moderação automática continua desligada — ligada, ela disputaria os dois núcleos com o servidor principal no meio de um checkout. O limite de 1,5 núcleo no contêiner protege o resto, mas também quer dizer que a moderação, hoje, não tem onde rodar.

O segundo é o pool de conexões do servidor principal. O código já aceita DB_POOL_SIZE, mas produção não define a variável, e o padrão fica em 5 conexões. Foi com esse 5 que o teste de carga de 26/08 registrou 74 transações recusadas por tempo de espera — com o banco usando 9 ou 10 conexões de 100. O banco tinha folga; a fila era de dentro do processo.

Cluster não resolve — piora

Ligar mais instâncias de cada serviço não multiplica processadores: cria mais processos disputando os mesmos 2 núcleos, cada um com sua própria cópia da memória. No serviço de mídia, cada instância carregaria os modelos outra vez.

Onde afeta

No aplicativo inteiro, sempre que o movimento aperta: lentidão na compra quando a mídia processa e transação recusada quando mais de cinco requisições do core disputam o banco ao mesmo tempo. E impede, na prática, ligar a moderação automática de conteúdo — que é uma proteção de marca, não um enfeite.

A solução
  1. Definir DB_POOL_SIZE em Production, fazendo a conta que o próprio prisma-client.ts descreve (instâncias × pool + workers + migrate + reserva, dentro das 150 conexões). É uma variável de ambiente e um deploy.
  2. Desligar o chat legado, se ele ainda estiver de pé: memória e processo que não atendem mais ninguém.
  3. Contratar um segundo servidor para mídia, chat e dashboard, deixando o servidor principal sozinho. Com moderação ligada, 4 núcleos e 16 GB é o mínimo confortável; sem ela, 2 e 8 bastam. A mudança nas esteiras é o rótulo do executor — uma linha em cada.
  4. Mover a dashboard primeiro, como ensaio de risco baixo; depois chat e mídia, que já têm endereço próprio.
  5. Ligar a moderação na máquina nova e medir.
A favor do prazo

Banco de dados e fila de mensagens de produção já ficam fora dessa máquina. Os serviços não guardam estado local, então mudá-los de servidor é reinstalar e apontar o endereço — não migrar dados.

Considerações

O pool é o passo mais barato da lista inteira e resolve um erro já medido. A segunda máquina é o que destrava a moderação e a redundância do item 07 — e, como endereço, certificado e banco da mídia e do chat já existem, o trabalho que sobra é quase só de deploy.

07

Nenhum serviço tem redundância

Médio todos os serviços · ecosystem.config
O que é

Todos os serviços rodam como um único processo, sem cópia de reserva. Se ele cai, reinicia, ou estoura o limite de memória configurado, o serviço fica fora do ar até subir de novo — alguns segundos em que nenhum colecionador é atendido.

E parte do código já depende disso: a varredura do sorteio avisa, no próprio arquivo, que só é segura com o core em instância única — duas instâncias rodariam duas varreduras disputando as mesmas linhas. Antes de duplicar, ela precisa de uma trava compartilhada.

O problema

Não é só o risco de queda: é que toda publicação de versão é uma pequena indisponibilidade. Com processo duplicado, dá para atualizar um de cada vez e ninguém percebe. Com processo único, toda entrega interrompe o atendimento.

Onde afeta

No servidor principal, onde a interrupção atinge todos os colecionadores ao mesmo tempo. E na frequência de publicação: quando cada entrega custa uma pequena queda, o time naturalmente entrega menos vezes — o que torna cada entrega maior e mais arriscada.

A solução

Depende do item 06 estar resolvido. Com 2 núcleos e quatro serviços já disputando, duplicar hoje pioraria as coisas. Depois de separar as máquinas, o servidor principal fica sozinho e pode rodar em duas cópias, com atualização alternada — desde que as varreduras agendadas, como a do sorteio, ganhem trava compartilhada e o DB_POOL_SIZE seja recalculado para as duas cópias.

Considerações

A mudança de configuração é de poucas linhas — quase todo o trabalho é teste. Só faz sentido depois da separação de servidores do item 06.

Aplicativo

3 itens
08

O aplicativo só avisa quando fecha — o resto do erro passa calado

Médio azcollections-mobile · Crashlytics · Bugsnag
O que é

O aplicativo reporta hoje só o que o derruba: o Crashlytics captura o fechamento inesperado pelo lado nativo, com os símbolos que tornam a pilha legível, e o código escreve nele um único dado de contexto — quantos vídeos estão vivos na tela. Todo o resto passa calado: nenhum erro tratado é registrado (não há uma chamada de recordError no código), nenhuma tela crítica é marcada e nenhum aviso dispara quando a taxa de erro sobe.

E há duas ferramentas para o mesmo trabalho: o Bugsnag continua instalado, com chave no app.json, ao lado do Crashlytics — sem nenhuma linha de código que o use.

O problema

No servidor dá para ler os registros e investigar. No aplicativo, não: ele roda no aparelho do colecionador, em versões de sistema e modelos que ninguém tem à mão. A maior parte dos defeitos não fecha o app — é uma compra que falha, uma tela que fica vazia, um envio que não sai. Sem reporte desses erros, um defeito que atinge um aparelho específico pode durar semanas invisível, e a única forma de descobrir é o colecionador reclamar — ou avaliar mal na loja.

A ferramenta a mais cobra à parte: é dependência nativa para manter atualizada e uma dúvida para quem chega — qual dos dois painéis é o de verdade.

Onde afeta

Na capacidade de reagir depois de cada publicação. Não impede publicar — define quanto tempo se leva para descobrir que algo deu errado.

A solução
  1. Registrar no Crashlytics os erros que não derrubam o app, a começar pelos fluxos que movem dinheiro — checkout e pagamento —, associando cada um à versão do aplicativo.
  2. Marcar as telas críticas — login, checkout, chat — para saber onde o erro aconteceu.
  3. Configurar aviso automático quando a taxa de erro subir depois de uma publicação.
  4. Decidir entre Bugsnag e Crashlytics e tirar o outro do projeto.
Considerações

A captura de crash já está de pé; o trabalho é registrar o erro que não derruba, marcar as telas críticas e conferir que ele chega. É o que permite enxergar o que acontecer depois de cada publicação.

09

Funcionalidades desligadas sem data para voltar

Baixo azcollections-mobile · marketplace/detail
O que é

Duas partes da página do produto estão comentadas no código, com o motivo registrado:

  • Avaliações — a lista de depoimentos saiu da ficha do produto. A nota do vendedor continua visível, só a lista saiu.
  • Selo de autenticidade — removido porque prometia uma verificação que não existe: hoje quem declara o estado e a autenticidade da peça é o próprio vendedor.

Os dois são desvios conscientes do design aprovado, e estão documentados. Não é código esquecido — é decisão registrada.

O problema

Código comentado envelhece mal. Ele continua sendo lido, continua confundindo quem chega, e vai ficando incompatível com o resto — até que religar deixe de ser descomentar e passe a ser reescrever.

O caso do selo de autenticidade é o mais interessante: a decisão de removê-lo foi acertada, porque a promessa era falsa. O débito não é o código comentado — é a funcionalidade que falta por trás dele.

Onde afeta

Pouco no dia a dia. Afeta a confiança do comprador — avaliações e selo de procedência são o que sustenta a decisão de comprar de um desconhecido — e a fidelidade ao design aprovado.

A solução
  1. Decidir, com o produto, se cada uma volta. São decisões de negócio, não técnicas.
  2. As avaliações são de vendedor, não de produto: se voltarem, o lugar natural é o perfil público dele.
  3. O selo de autenticidade só deve voltar acompanhado de alguma verificação real — senão o débito volta junto.
  4. O que não voltar, apagar: a decisão fica registrada na documentação, e o código para de confundir.
Considerações

Dominado pela decisão, não pela execução: religar é descomentar, remover é apagar. O que trava é definir se cada uma volta — e o selo de autenticidade só deveria voltar acompanhado de uma verificação real.

10

O runtimeVersion parou em 3.0.0 — e o aplicativo já está na 3.0.4

Alto azcollections-mobile · app.json · runtimeVersion
O que é

O aplicativo tem atualização pelo ar ligada (expo-updates, canal production), e é o runtimeVersion que decide quais binários instalados recebem cada pacote publicado. O objetivo é simples: ele deveria acompanhar o número da versão do aplicativo — a build 3.0.4 com runtime 3.0.4 —, para que um eas update só chegue a binários com a mesma camada nativa. Hoje não acompanha: está parado em 3.0.0 desde a primeira versão do 3.0, enquanto a parte nativa mudou entre as versões que estão nas mãos dos colecionadores.

app.json na staging (de onde sai a produção) e na dev — iguais"version": "3.0.4" "runtimeVersion": "3.0.0" ← o mesmo desde a 3.0.0 dependências nativas que entraram depois da 3.0.1 (31/08) expo-glass-effect (09/09) · react-native-play-install-referrer (16/09) expo-document-picker (20/09) · expo-audio · expo-haptics (23/09)
O problema

Para o serviço de atualização, todos os binários da 3.0.0 à 3.0.4 são intercambiáveis. Um eas update publicado a partir da staging de hoje seria entregue também a quem está na 3.0.1 — e o código novo chamaria módulos nativos que aquele binário não tem. Não é degradação: é o aplicativo fechando na abertura, para quem não pediu nada.

É exatamente o risco que motivou subir o runtimeVersion de 2.0.0 para 3.0.0 na estreia do 3.0, para proteger os binários 2.x. A proteção foi feita uma vez e não acompanhou as versões seguintes; o script de versão, por decisão registrada, não mexe nele.

Onde afeta

Em todo colecionador que ainda não atualizou pela loja, no dia em que alguém publicar uma correção pelo ar — que é justamente a ferramenta usada quando há pressa. Hoje nada impede esse comando; a única defesa é lembrar.

A solução
  1. Fazer o runtime seguir a versão: trocar o valor fixo pela política appVersion do Expo ("runtimeVersion": { "policy": "appVersion" }), que usa o próprio version do app.json — 3.0.4 passa a ter runtime 3.0.4, e cada versão da loja recebe só as atualizações feitas para ela.
  2. Publicar a próxima versão da loja já com a política nova, para que os binários daqui em diante fiquem separados.
  3. Até lá, tratar o eas update no canal production como proibido.
Considerações

A mudança é de uma linha no app.json, e só vale a partir da próxima build nas lojas: os binários que já estão instalados continuam todos no runtime 3.0.0. O custo real é aceitar a regra — correção pelo ar só alcança quem está na mesma versão; quem precisa de nativo novo atualiza pela loja.

Consistência e documentação

1 item
11

Quatro serviços, quatro versões de Prisma

Médio core · mídia · chat · dashboard
O que é

Os quatro serviços acessam banco com a mesma ferramenta, em quatro versões diferentes — três delas com anos de diferença entre si:

versão do Prisma por serviçoservidor principal 5.14 serviço de mídia 5.22 chat 6.8 dashboard 7.4
O problema

O conhecimento não transfere. A forma de escrever uma migração, o comportamento das transações e as mensagens de erro mudam entre versões maiores — então cada serviço tem particularidades próprias, e o que se aprendeu em um não vale no outro.

E o custo cresce sozinho: quanto mais tempo passa, maior o salto de versão acumulado e mais caro fica atualizar. É o exemplo mais literal de juros da lista.

Onde afeta

Em quem trabalha em mais de um serviço — o que, num time pequeno, é todo o time. E aparece com força sempre que uma entrega precisa de migrações em mais de um serviço, rodando em ordem.

A solução
  1. Escolher a versão alvo — a mais nova já em uso, a da dashboard.
  2. Subir um serviço por vez, começando pelo de mídia, que é o menor e o mais isolado.
  3. O servidor principal por último, porque é o que tem mais superfície e mais risco.
  4. Fazer cada subida sozinha, sem migração de schema junto. Atualizar ferramenta de banco na véspera de uma migração grande é somar dois riscos sem necessidade.
Considerações

Tarefa mecânica, com guia de migração público — o tipo em que a assistência de IA rende bem. O que importa é quando: numa entrega própria, nunca junto de uma migração grande.

Sorteio e dados do colecionador

2 itens
12

Os sinais de risco do sorteio nunca são anulados

Médio api.collectionsaz.com · src/v3/raffle · raffleParticipationRequirements
O que é

Ao entrar no sorteio, o core guarda três sinais de risco para o antifraude: a marca do aparelho, a marca da faixa de rede e a idade da conta. A documentação do sorteio e o próprio schema prometem que eles são anulados 90 dias depois do sorteio — servem ao antifraude daquele sorteio e a nada mais. A coluna que registraria a anulação existe; nada a preenche.

core, dev (24/09/2026)schema.prisma signalsPurgedAt DateTime? /// Anulados 90 dias depois do sorteio escritas de signalsPurgedAt em src/ 0 raffle-sweeper abre · fecha · sorteia · avisa — não limpa nada deviceHash · networkHash · accountAgeDays gravados na entrada, nunca zerados
O problema

É uma promessa de retenção escrita e não cumprida. Os sinais são pseudonimizados, não anônimos: combinados, ainda dizem algo sobre o aparelho e a rede de cada colecionador que participou. Guardá-los para sempre contraria a finalidade declarada — e a LGPD pede exatamente isso, que o dado viva só enquanto serve à finalidade.

Onde afeta

Em todo colecionador que já entrou num sorteio. O volume cresce a cada edição, e o prazo de 90 dias de cada sorteio realizado passa sem que nada aconteça.

A solução
  1. Acrescentar à varredura do sorteio um passo de retenção: para os sorteios realizados há mais de 90 dias, zerar deviceHash, networkHash e accountAgeDays e carimbar signalsPurgedAt.
  2. Fazer o passo idempotente e em lote — a varredura roda a cada minuto, e a anulação só precisa acontecer uma vez por linha.
  3. Mostrar no painel do sorteio quando os sinais já foram anulados, para ninguém estranhar a coluna vazia.
Considerações

Pequeno e isolado: a coluna já existe e a varredura já roda. O que torna o item urgente não é o tamanho, é que a promessa está escrita — e cada sorteio novo aumenta o que precisa ser limpo.

13

Apagar uma conta apagaria junto os bilhetes e o resultado do sorteio

Médio api.collectionsaz.com · prisma/schema.prisma · raffleTicket · raffleResult
O que é

Bilhetes e resultados do sorteio estão presos à conta do colecionador com exclusão em cascata. Se a linha do usuário for apagada de verdade, o banco apaga junto os bilhetes dele — e, se ele tiver ganhado, o próprio resultado. E o resultado não guarda uma cópia do nome do ganhador: sem a conta, não sobra registro de quem ganhou.

prisma/schema.prisma do coreraffleTicket user … onDelete: Cascade (linha ~2737) raffleResult user … onDelete: Cascade (linha ~2853) userId · ticketNumber · revealedSeed — sem nome do ganhador também em cascata: raffleGrant · raffleParticipationRequirements

Hoje isso não acontece porque a exclusão de conta é soft delete — a linha fica, marcada como apagada. O único apagamento físico de usuário no código é o script de limpeza das contas de convidado, que não participam de sorteio.

O problema

O schema diz o contrário do que o sorteio promete: “vencedor nunca se edita” e a conta pode ser refeita por qualquer um a partir da semente revelada. Uma exclusão física — um pedido de apagamento pela LGPD atendido direto no banco, um script de limpeza novo — faria o resultado sumir, e a auditoria pública do sorteio passaria a apontar para um bilhete que não existe mais.

Onde afeta

No histórico e na auditoria de todo sorteio já realizado, e na forma de atender um pedido de exclusão de dados. A proteção atual é uma convenção — ninguém apagar usuário de verdade —, não uma regra do banco.

A solução
  1. Trocar a relação de raffleResult e raffleTicket com o usuário para onDelete: Restrict (ou SetNull, com o userId opcional), para que o banco recuse apagar a conta de quem tem bilhete ou resultado.
  2. Copiar para o resultado, no momento do sorteio, o que precisa sobreviver à conta — o nome público do ganhador.
  3. Definir o que a exclusão de conta faz com o sorteio: anonimizar o vínculo em vez de apagar a linha, preservando número do bilhete e resultado.
Considerações

Uma migration pequena, mas com uma decisão por trás: o que o sorteio guarda de um colecionador que pediu para sair. Vale tomá-la antes que a primeira exclusão física aconteça, porque depois não há o que recuperar.

Em que ordem pagar

A lista não precisa ser paga de uma vez — e não deve. A ordem abaixo segue a severidade e o dano possível, e dentro de cada bloco começa pelo que custa menos. O que é alto e grande não vira projeto próprio: é pago aos poucos, nas entregas normais.

Quatro blocos, em ordem de prioridade.
Bloco Itens Por quê
Alto e barato — primeiro 10 · 06 · 04 Os três cobram de uma vez, sem aviso: uma atualização pelo ar que derruba quem está numa versão anterior, transações recusadas quando o movimento aperta, um relatório segurando o banco do aplicativo. E os três começam com uma linha de configuração — o runtimeVersion, o DB_POOL_SIZE, o teto de tempo da dashboard. A segunda máquina e a réplica vêm em seguida.
Médio que protege ou destrava 03 · 12 · 13 · 08 O 03 devolve a confiança nos testes e é a rede para o bloco seguinte. O 12 e o 13 são promessas feitas ao colecionador — retenção e auditoria do sorteio — que o código ainda não cumpre. O 08 define quanto tempo se leva para descobrir um erro no aparelho.
Alto, diluído nas entregas 01 · 02 Cobram juros todo dia, mas não viram projeto próprio. A fronteira de arquitetura entra com a aprovação do ADR-001 e a próxima funcionalidade do servidor; cada tarefa que encostar num serviço-monstro paga a extração daquele pedaço.
Quando houver motivo 07 · 05 · 11 · 09 O 07 depende do 06. O 05 cresce com cada funcionalidade nova do core, mas só pede tela quando o atendimento apontar. O 11 é mecânico e cabe numa entrega própria. O 09 é decisão de produto, não de engenharia.
Se só houver espaço para um

O item 03. Não é o mais grave, mas é o único que torna todos os outros mais seguros: sem uma suíte de testes em que se confie, quebrar os serviços-monstro do item 02 — ou mexer no sorteio dos itens 12 e 13 — é trabalhar sem rede.