Enviando E-mails Transacionais em um App Construído com IA
Resposta rápida
Não envie e-mails transacionais direto do seu request handler com uma chamada SMTP fixa no código. Use uma API de e-mail dedicada (Resend, Postmark, SES), autentique seu domínio de envio com SPF, DKIM e DMARC antes de colocar no ar, e jogue o envio de verdade numa fila para que um provedor de e-mail lento nunca segure o fluxo de cadastro ou checkout que disparou o envio.
Por que "simplesmente chamar sendEmail() na rota" quebra
Começa de forma inocente. Um usuário se cadastra, sua rota de API cria a conta e então chama o provedor de e-mail para disparar uma mensagem de boas-vindas, tudo na mesma requisição. Funciona nas primeiras dez vezes que você testa.
Aí o seu provedor de e-mail tem um minuto lento — provedores têm, até os bons — e essa mesma requisição agora leva seis segundos a mais porque seu servidor fica esperando uma resposta 202 que na verdade não precisava ser síncrona. Ou pior: a conta é criada, a chamada de e-mail lança um erro, e seu tratamento de erro não desfaz nada, então o usuário tem uma conta mas nunca recebeu o link de confirmação. Ele acha que o cadastro falhou e tenta de novo.
Já vi exatamente esse bug em três bases de código diferentes. Ninguém escreveu de propósito. É só o que acontece quando "enviar um e-mail" é tratado como uma escrita no banco de dados em vez do que realmente é: uma chamada a um serviço de terceiros que você não controla, que pode ser lento, pode falhar, e não deveria ter o poder de travar nada que o usuário esteja esperando.
O sinal de que você está fazendo errado
Se uma redefinição de senha, um recibo ou um e-mail de boas-vindas é enviado por código dentro da mesma função que trata a requisição do usuário, você acoplou o uptime do seu app ao uptime do seu provedor de e-mail. Isso está invertido — um desses dois deveria poder ter um dia ruim sem derrubar o outro.
O padrão que realmente aguenta
Uma configuração de e-mail transacional que resiste a tráfego de verdade tem três peças trabalhando juntas, e nenhuma delas é exótica.
Autenticação de domínio, feita antes de enviar um único e-mail de verdade. SPF e DKIM dizem aos servidores de e-mail que recebem que seu app realmente tem permissão para enviar como voce@seudominio.com. O DMARC diz o que fazer se essa checagem falhar. Pule isso e seus primeiros cem e-mails caem na caixa de spam, ponto final — Gmail e Outlook não dão mais o benefício da dúvida para remetentes não autenticados. É uma configuração de DNS única, uns vinte minutos, e provedores como Resend e Postmark te guiam pelos registros exatos a adicionar.
Uma fila entre "algo aconteceu" e "e-mail enviado". O handler de cadastro não chama a API de e-mail — ele grava um job (send-welcome-email, userId: 123) em algum lugar durável, e um worker separado pega e envia. Se o provedor de e-mail cair por dois minutos, o job só espera e tenta de novo. A requisição de cadastro do usuário já voltou com 200 e seguiu em frente.
Idempotência no próprio envio. Retentativas são boas até um job rodar duas vezes e o mesmo cliente receber dois e-mails de "seu pagamento falhou" com quatro minutos de diferença. Anexe uma chave única a cada job — um ID de pedido, um token de redefinição de senha — e verifique essa chave antes de enviar, para que um job repetido que já teve sucesso seja um no-op, não uma duplicata.
E-mail transacional e e-mail de marketing não são o mesmo problema
É tentador rotear tudo — e-mails de boas-vindas, resumos semanais, redefinições de senha — pelo mesmo sistema. Não faça isso. Uma redefinição de senha precisa chegar em menos de um minuto ou o usuário já desistiu e clicou em "reenviar". Um resumo semanal pode esperar uma hora que ninguém percebe. Misturar os dois no mesmo domínio de envio também é um risco de entregabilidade: se seus e-mails de marketing forem marcados por reclamações de spam, você não quer que isso prejudique a reputação do domínio da qual suas redefinições de senha dependem. A maioria dos times acaba com um subdomínio transacional (mail.seuapp.com) separado do que envia a newsletter.
Uma comparação: envio direto vs. envio em fila
| Aspecto | Envio direto na rota | Envio em fila com worker |
|---|---|---|
| Latência percebida pelo usuário | Atrelada ao tempo de resposta do provedor de e-mail | Sem impacto — a requisição retorna na hora |
| Comportamento quando o provedor está fora do ar | A requisição falha ou trava | O job espera e tenta de novo automaticamente |
| Risco de e-mails duplicados | Alto em qualquer lógica de retry acima | Baixo, se o job carrega uma chave de idempotência |
| Complexidade de configuração | Menor no início (uma chamada de API) | Um pouco maior (uma fila e um worker) |
| Onde quebra | No primeiro dia lento do provedor | Raramente — é assim que caixas de entrada de verdade veem o padrão |
Onde isso vive em um app construído com a Greta
Como a Greta monta jobs em segundo plano como parte nativa do stack — o mesmo padrão de trabalho em fila coberto em rodando jobs em segundo plano e tarefas agendadas — conectar e-mail transacional significa adicionar um tipo de job, não construir uma fila do zero. Uma rota como app/api/webhooks/postmark/route.ts pode até tratar eventos de bounce e reclamação da mesma forma que trata qualquer outro webhook de entrada, o padrão que vimos para verificação de assinatura e tratamento idempotente em conectando webhooks e APIs de terceiros.
Os próprios templates de e-mail — redefinição de senha, recibo, convite — vivem como funções pequenas e testáveis que recebem dados e devolvem HTML, então dá para revisar e ajustar o texto sem tocar na lógica de envio.
Perguntas frequentes
Qual provedor de e-mail eu deveria usar? Para um app novo, Resend ou Postmark são boas escolhas — APIs limpas, boa entregabilidade de fábrica, planos gratuitos razoáveis. O SES é mais barato em volume real, mas você assume mais do trabalho de entregabilidade sozinho. Escolha um, autentique o domínio direito, e não pense demais além disso.
Preciso de uma fila logo no primeiro dia, mesmo sendo um fundador solo? Não necessariamente para os primeiros cinquenta usuários, mas no momento em que você tem cadastros de verdade, uma resposta lenta do provedor vira um cadastro lento para uma pessoa de verdade. É pouca configuração para adicionar antes que importe, contra uma configuração estressante para adicionar depois de um chamado de suporte sobre um cadastro travado.
Como evito que e-mails transacionais caiam no spam? Autentique o domínio (SPF, DKIM, DMARC), mantenha o tráfego transacional e o de marketing em subdomínios separados, e envie de um endereço que as pessoas reconheçam — endereços no-reply@ prejudicam o engajamento, o que depois prejudica a entregabilidade. Aqueça um domínio de envio novo aos poucos em vez de disparar volume alto desde o primeiro dia.
E os e-mails que precisam ser enviados em tempo real, tipo um login por link mágico? Esses também podem passar pela fila — um worker bem configurado pega um job em um ou dois segundos, o que é rápido o suficiente para o usuário não perceber o salto extra, e você mantém as mesmas garantias de confiabilidade em vez de criar uma exceção que vira o único caminho de código que ninguém lembra de corrigir depois.
Encerramento
E-mail parece a integração mais fácil de todo o app — uma chamada de API e pronto. Também é a integração com mais chance de falhar silenciosamente de um jeito que ninguém percebe até um cliente dizer "nunca recebi meu recibo". Autentique o domínio, coloque o envio em fila, torne idempotente, e ele vira uma das partes mais chatas e confiáveis do seu stack em vez daquela que você tem medo de tocar.
