10 dias no invisível: construindo um sistema de e-mails que mantém a produção
10 dias. 30 commits. Zero novas funcionalidades visíveis. Enquanto os utilizadores do TAMSIV esperavam um novo botão, uma nova cor ou uma nova função de voz, passei cada dia a reescrever algo que ninguém jamais verá: a forma como a aplicação fala com as pessoas por e-mail. E percebi que o invisível talvez represente 80% do que faz com que um produto permaneça instalado.
Pontos-chave a reter
- Um sistema completo de e-mails transacionais: lembrete de verificação, feedback loop D+7, preferências, cancelamento de subscrição limpo, histórico por utilizador.
- Um webhook Resend que escuta os 6 tipos de eventos (delivered, opened, clicked, bounced, complained, delivery_delayed) para agregar as métricas em tempo real.
- Um fluxo anti-usurpação RGPD: um utilizador pode reportar um e-mail de boas-vindas que nunca pediu, e a inscrição é imediatamente eliminada.
- Um cron diário que envia um lembrete único para contas não verificadas há 3 dias, i18n 6 idiomas.
- Dois hotfix mobile em paralelo (v1.07 + v1.08): auditoria gesture-handler, redesenho da UI do feed, modais estáveis, 8 bugs corrigidos.
Por que construir um sistema de e-mails inteiro quando o Supabase Auth já envia os e-mails de verificação
É uma questão legítima. O Supabase, na sua configuração padrão, já envia um e-mail de verificação a cada registo. Muitos produtos param por aí e não adicionam mais nada. Isso é suficiente para validar um e-mail, mas não para construir uma relação com o utilizador.
Em produção desde 4 de abril, deparei-me com dezenas de contas criadas mas não verificadas. Sem acompanhamento. Sem feedback no D+7 para saber se a aplicação lhes era útil. Sem uma forma limpa para alguém dizer "este e-mail não era para mim". E, acima de tudo, nenhuma visibilidade do lado do administrador sobre o que era enviado, chegava, rebatia ou era marcado como spam.
Um sistema de e-mail transacional interno é exatamente o que separa um produto "tecnicamente funcional" de um produto que parece sério. As grandes aplicações escondem-no por trás do seu polimento. As pequenas aplicações negligenciam-no e perdem utilizadores sem entender porquê.
O cron diário que envia um lembrete, e APENAS UM
O primeiro tijolo colocado é um cron diário que executa a uma hora fixa no Vercel. Ele consulta a base de dados, seleciona as contas criadas há exatamente 3 dias e ainda não verificadas, e aciona um lembrete único por utilizador.
[EXPERIÊNCIA PESSOAL] A regra "apenas um lembrete" é voluntária. Recebi mais e-mails de acompanhamento abusivos do que consigo contar. Três lembretes em 48h, cinco numa semana, dez num mês. É a melhor forma de fazer as pessoas cancelarem a subscrição antes mesmo de experimentarem o produto.
O cron escreve numa tabela dedicada para saber quem recebeu o quê e quando. Se um utilizador já foi lembrado, é ignorado. Se o envio do Resend falhar, o erro é registado, mas o cron não tenta novamente automaticamente: prefiro visibilidade sobre as falhas a uma saturação silenciosa.
O feedback loop no D+7: 4 botões, 4 verdades
[INSIGHT ÚNICO] Sete dias após a inscrição, cada novo utilizador recebe um e-mail com 4 botões clicáveis: adoro, odeio, tenho uma sugestão, encontrei um bug. Cada botão aponta para uma página dedicada que regista a resposta e oferece uma área de comentário livre.
Por que esta forma precisa? Porque uma classificação na Google Play é filtrada por quem tem mais energia para a deixar. Um e-mail com 4 botões capta a verdade silenciosa. A das pessoas que nunca irão à loja escrever um comentário, mas que com um clique podem dizer "não gostei".
Tecnicamente, cada botão contém um token assinado relativo ao utilizador e ao tipo de feedback. A rota GET no site valida o token, regista a resposta com o user_id e o tipo, e exibe a página correspondente com um formulário livre. Nenhuma autenticação adicional para deixar um feedback: é intencional. A fricção mata o feedback.
O fluxo anti-usurpação: RGPD sem drama
Caso concreto: alguém cria uma conta TAMSIV com o e-mail alice@exemplo.fr. Mas a Alice não pediu nada. Ela recebe um e-mail de boas-vindas para uma conta que nunca criou. O que ela faz?
Sem este fluxo, ela coloca o e-mail no spam ou reporta o remetente. Em ambos os casos, é uma perda: perda para mim em termos de reputação de envio, perda para ela que não tem como encerrar o assunto, perda para a pessoa que cometeu o erro de digitação e que nunca mais receberá comunicação no endereço correto.
Na nova versão, cada e-mail de boas-vindas contém um link "não criei esta conta". Clica, página dedicada, confirmação. A inscrição é imediatamente eliminada da base de dados, um registo é armazenado para auditoria, e uma mensagem final confirma à Alice que o seu endereço não será mais utilizado. RGPD sem drama. Uma ação, três segundos, está feito.
[DADOS ORIGINAIS] A página está traduzida nas 6 línguas da aplicação (francês, inglês, alemão, espanhol, italiano, português). Os caracteres acentuados estavam quebrados em algumas línguas devido a um problema de codificação no ficheiro de tradução: correção no commit 12e61a7.
O webhook Resend: saber antes que o utilizador escreva
Todos os envios passam por Resend, um provedor de e-mails transacionais focado na experiência do desenvolvedor. O Resend oferece webhooks para cada evento do ciclo de vida de um e-mail: email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.delivery_delayed.
Todos são escutados e armazenados numa tabela dedicada com a referência ao utilizador, o tipo de e-mail, o carimbo de data/hora e os metadados associados. Isso permite depois agregar: quantos e-mails chegaram hoje, quantos foram abertos, quais geraram um clique, quantos saltaram.
O painel de administração exibe essas estatísticas em tempo real com distintivos que se incrementam a cada envio. Um bounce que aparece para um utilizador? Vejo-o imediatamente, posso verificar se é um problema temporário ou um e-mail morto, e ajustar. Uma reclamação (denúncia como spam)? Prioridade alta, investigação imediata.
O histórico de envio agrupado por utilizador, com reenvio e dedup
O administrador vê todos os envios recentes, mas agrupados por utilizador. Se a Alice recebeu o seu e-mail de verificação, depois o seu lembrete D+3 e depois o seu feedback D+7, vejo uma linha "Alice" com três sub-linhas em expansão. Mais legível do que um fluxo cronológico bruto.
Cada envio tem um distintivo "fonte" (automático / manual). Os envios automáticos (cron, triggers) são diferenciados dos envios manuais (botão "reenviar" no administrador). Um botão "reenviar" existe em cada linha para os casos em que um e-mail chegou ao spam, para forçar uma nova tentativa para outro alias, ou para testar uma alteração de modelo.
Um sistema de dedup impede o re-spam: se enviei um e-mail de boas-vindas há 2 horas e quero fazer um envio em grupo "todas as inscrições do dia", a Alice é excluída automaticamente porque já recebeu. Um distintivo "elegível" é exibido para cada utilizador para indicar se ele pode receber o envio atual ou não.
Dois hotfix mobile em paralelo: v1.07 e v1.08
Enquanto o backend de e-mail avançava no Vercel, a aplicação móvel precisava de ar. Duas versões foram lançadas na Play Store em Alpha, depois em produção.
v1.07 (7d65fd0): 8 bugs específicos, mais uma nova vista compacta nas pastas. Quando tu tens 15 sub-pastas num projeto, queres ver a árvore de uma só vez sem rolar. Um botão alterna entre a vista detalhada (cartões completos com miniaturas e pré-visualizações) e a vista compacta (linhas densas com apenas o nome e a contagem). Zero funcionalidades novas na aparência, mas uma mudança de uso para os utilizadores intensivos.
v1.08 (a494e7e): auditoria completa gesture-handler. O TAMSIV usa react-native-gesture-handler em todo o lado, mas vários ecrãs ainda tinham TouchableOpacity ou FlatList importados diretamente de react-native. Resultado: toques intermitentes que não passavam, impossíveis de reproduzir, reportados por utilizadores que acabavam por desinstalar sem entender.
A auditoria afetou mais de 25 ficheiros. Cada TouchableOpacity, cada FlatList, cada ScrollView foi alterado para a importação de gesture-handler. O bug dos toques fantasmas foi corrigido. Aproveita para redesenhar a UI do feed e estabilizar todos os modais: de passagem, o polimento dá um salto significativo.
O que aprendi depois de 10 dias
O que me impressionou foi que cada linha de código escrita durante estes 10 dias permanecerá invisível enquanto funcionar. Ninguém diz "uau, o teu e-mail de verificação chegou na hora certa, apenas uma vez, com o idioma correto". Ninguém nota que um bounce foi detetado automaticamente e que o administrador viu o sinal antes mesmo de o utilizador escrever.
O invisível só é notado quando falha. E nesse momento, o utilizador não entende o que quebrou: ele apenas entende que a aplicação "não funciona bem". Ele desinstala. Ele não escreve. Ele não diz porquê.
Portanto, o invisível talvez seja 80% do que faz um produto acabado. Não os botões que adicionamos em lançamentos de funcionalidades, não as cores que refazemos, não as animações. Apenas o facto de que, quando algo deve chegar ao utilizador, chega. Na hora certa. Uma vez. No idioma certo.
Não é sexy para postar no LinkedIn. Mas é o que faz um produto permanecer instalado.
FAQ
Por que não usar um serviço tudo-em-um como Mailchimp ou ConvertKit?
Porque são ferramentas de newsletter, não transacionais. São concebidos para envios editoriais em grupo, não para gatilhos individuais ligados ao ciclo de vida do utilizador. O Resend é concebido para e-mails transacionais (boas-vindas, verificação, redefinição de palavra-passe, feedback loop). É a ferramenta certa para o trabalho certo.
O webhook Resend é fiável para os eventos?
Muito fiável. O Resend tenta novamente os webhooks em caso de falha, assina os payloads para autenticação e expõe uma consola para reproduzir manualmente. Em 10 dias de uso, não detetei um único evento perdido.
Como gerir as traduções dos 6 idiomas para os e-mails?
Cada modelo de e-mail é definido em francês, depois traduzido por um script automático via OpenRouter (LLM). As chaves de tradução vivem nos mesmos ficheiros messages/*.json que o site, com um namespace emails dedicado. O envio do lado do servidor usa o idioma preferido do utilizador armazenado na base de dados, com fallback para inglês se ausente.
O fluxo anti-usurpação é abusivo? É possível eliminar a conta de outra pessoa?
Não. O link "não criei esta conta" é assinado com um token ligado ao endereço de e-mail alvo. Apenas a pessoa que recebe o e-mail pode clicar nele e eliminar a inscrição. Se outra pessoa tentar aceder à rota sem o token correto, ela retorna um erro 403 sem fazer nada.
Este sistema aplica-se ao iOS quando a aplicação for lançada?
Sim, é backend. Os e-mails partem do Vercel e não dependem da plataforma da aplicação móvel. No dia em que o TAMSIV for lançado no iOS (12 pessoas já clicaram em "Descarregar para iOS" no site), o fluxo de e-mails será idêntico.