Arquitetura de um Gateway de E-mails Transacionais: Por que separamos API de Workers, Argon2id com prefixo e rotação de chaves
Disparar e-mails transacionais diretamente de dentro dos endpoints da sua aplicação cria acoplamento excessivo, eleva o tempo de resposta HTTP (handshakes SMTP levam facilmente entre 500ms e 2s) e espalha credenciais sensíveis por vários microsserviços.
O Hermes é um gateway open-source e multi-tenant desenvolvido para centralizar esse fluxo. Para evitar que a lentidão ou instabilidade de provedores SMTP trave as requisições upstream, o sistema separa rigidamente a recepção HTTP do envio real utilizando filas assíncronas com BullMQ e Redis. No campo da segurança, as credenciais SMTP em repouso são protegidas com AES-256-GCM, enquanto as chaves de API programáticas utilizam uma estratégia híbrida de prefixo indexado + Argon2id para garantir buscas em sem abrir mão de proteção contra ataques de força bruta por hardware.
Os repositórios e o pacote já estão disponíveis:
- API Gateway e Workers: github.com/RuanLopes1350/hermes-api
- Painel Administrativo: github.com/RuanLopes1350/hermes-front
- SDK TypeScript (Client): github.com/RuanLopes1350/hermes-client
- Pacote no NPM:
@ruanlopes1350/hermes-client
1. O Problema que Resolvemos
Em arquiteturas tradicionais, é comum ver o transporte SMTP (usando bibliotecas como Nodemailer) instanciado diretamente no fluxo de criação de conta ou recuperação de senha:
[Requisição de Cadastro]
└──► Salva no Banco
└──► Conecta no SMTP (DNS + TLS Handshake + Envio) ──► Espera 1.5s ──► Responde 200 OK
Esse modelo gera três gargalos:
- Latência no cliente: O usuário final fica com o botão travado esperando a confirmação da rede externa de e-mail.
- Perda de mensagens: Se o servidor SMTP oscilar ou aplicar rate limit momentâneo, o e-mail se perde ou exige lógica de retry local complexa que pode sobrecarregar a própria API.
- Gestão de credenciais: Conforme a quantidade de microsserviços cresce, senhas de app e tokens de envio ficam dispersos em múltiplos arquivos
.env.
O Hermes atua como uma barreira isoladora com modelo multi-tenant: cada serviço cadastrado tem suas próprias chaves, templates e histórico de auditoria.
┌─────────────────────────────────────────────────────────────┐
│ APLICAÇÕES CLIENTES (@ruanlopes1350/hermes-client) │
└──────────────────────────────┬──────────────────────────────┘
(POST /api/emails + X-API-Key)│ ▲ (Webhook assinado
│ │ para rotação de chave)
▼ │
┌───────────────────────────────────────┴─────────────────────┐
│ HERMES API GATEWAY │
│ (Recepção rápida, validação de schema e enfileiramento) │
└──────────────┬──────────────────────────────┬───────────────┘
│ │
(Grava como 'pending' no banco) │ (Enfileira Job no BullMQ)
▼ ▼
┌──────────────────────────────┐ ┌────────────────────────────┐
│ POSTGRESQL (Drizzle) │ │ REDIS STORE │
│ (Serviços, Chaves, Templates)│ │ (Fila de Jobs e Pub/Sub) │
└──────────────────────────────┘ └─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ HERMES WORKER │
│ (Consome fila, descriptografa,│
│ compila MJML e envia SMTP)│
└─────────────┬──────────────┘
│
▼ (TLS / SMTP)
┌────────────────────────────┐
│ PROVEDOR SMTP / GMAIL │
└────────────────────────────┘
2. Por que Separamos a API do Worker?
A separação em processos Node.js independentes garante previsibilidade operacional:
API Gateway (server.ts)
A API tem uma única missão: receber o payload, validar o formato dos dados, autenticar a API Key, registrar o e-mail no PostgreSQL como pending e postar o job na fila do Redis.
- Tempo de resposta típico:
< 25ms. - A API retorna imediatamente
201 Createdcontendo o ID do e-mail para rastreamento. O endpoint que chamou o Hermes segue seu fluxo sem esperar a entrega de rede.
Email Worker (worker.ts)
O Worker opera em background consumindo a fila do BullMQ:
- Resgata a credencial de envio do tenant (senha SMTP pura ou token dinâmico via Google OAuth2/Gmail API).
- Descriptografa a credencial em memória.
- Compila o template MJML injetando as variáveis dinâmicas com Handlebars.
- Dispara a mensagem via Nodemailer.
- Em caso de sucesso ou falha definitiva, atualiza o status no PostgreSQL (
sentoufailed) com stack de erros e timestamp. - Emite um evento via Redis Pub/Sub, que alimenta as telas do painel administrativo em tempo real através de Server-Sent Events (SSE).
Se o provedor SMTP retornar erro temporário de rede, o BullMQ executa retentativas com backoff exponencial com jitter, sem impactar a capacidade da API de continuar recebendo novas requisições.
3. Decisões de Criptografia e Segurança
3.1 API Keys: O Dilema de Performance vs. Força Bruta (Argon2id)
Se salvarmos chaves de API com hash simples (como SHA-256), um vazamento de banco permite ataques de dicionário acelerados por GPU. Por outro lado, se usarmos um algoritmo moderno e pesado como Argon2id na chave inteira, como não há usuário informado na requisição, o servidor precisaria rodar o argon2.verify() contra todas as chaves do banco a cada requisição HTTP (), o que inviabilizaria o servidor.
A solução adotada divide a chave em duas partes no formato hm_prefix.secret:
hm_b5c92a10.e4d3c2b1a0f9e8d7c6b5a4938271605f...
└─────┬───┘ └──────────────────────┬──────────────────────┘
│ │ Segredo Aleatório (32 bytes em HEX - 64 caracteres)
└ Prefixo Público (8 caracteres HEX indexados no banco)
- Busca Indexada (): O middleware extrai o prefixo público (
hm_b5c92a10) e faz uma busca indexada no PostgreSQL (WHERE prefix = '...' AND is_active = true). Isso isola instantaneamente o registro exato. - Validação Forte: Com a linha recuperada, o sistema aplica o
argon2.verify(key_hash, secret)exclusivamente no segredo recebido. - Resultado: Busca ultra rápida no banco associada a uma barreira resistente contra ataques paralelos em hardware dedicado.
3.2 Credenciais em Repouso: Cifragem de Envelope com AES-256-GCM
Senhas de envio SMTP e tokens de refresh do Google OAuth2 nunca ficam em texto limpo no banco de dados. Eles são gravados no formato:
<iv_hex>:<auth_tag_hex>:<ciphertext_hex>
- AES-256-GCM: Fornece tanto sigilo quanto integridade autenticada.
- IV Dinâmico: Um vetor de inicialização de 16 bytes aleatório é gerado a cada gravação, garantindo que a mesma senha resulte em dados cifrados completamente distintos a cada update.
- Auth Tag: Impede adulterações silenciosas no banco de dados; qualquer bit alterado faz a descriptografia falhar.
- Master Key: A chave mestra reside unicamente nas variáveis de ambiente do servidor e nunca transita pela rede.
4. Rotação Automática de Chaves ("Webhook-First")
Trocar chaves de API manualmente em ambientes de produção costuma gerar indisponibilidade se o cliente não for atualizado no mesmo instante. O Hermes possui um mecanismo de rotação automática com tolerância a falhas:
- Um cronjob diário via BullMQ (
system.ts) busca credenciais ativas com data de expiração próxima do limite configurado (padrão: 3 dias). - O sistema gera a nova chave candidata e envia uma requisição
POSTHTTPS para o endpoint de webhook configurado no serviço do cliente. - A requisição leva o cabeçalho
X-Hermes-Signature, gerado via HMAC SHA-256 utilizando o segredo privado do serviço:const signature = crypto .createHmac('sha256', webhookSecret) .update(JSON.stringify(payload)) .digest('hex'); - Garantia de Entrega (Webhook-First): Se o webhook do cliente falhar (timeout ou erro 5xx), o banco de dados do Hermes não é alterado. A chave antiga continua funcionando normalmente e o BullMQ agenda uma nova tentativa.
- Atualização Efetiva: Apenas quando a aplicação cliente confirma com
200 OKo recebimento da nova chave, o Hermes comita o novo hash e expiração no PostgreSQL.
5. Integração com o SDK (hermes-client)
Para simplificar o consumo, publicamos o pacote @ruanlopes1350/hermes-client no NPM. Ele conta com interface fluente e middlewares para recepção do webhook de rotação:
import { HermesClient, MemoryAdapter } from '@ruanlopes1350/hermes-client';
const hermes = new HermesClient({
baseUrl: 'https://api-hermes.suaempresa.com',
storageAdapter: new MemoryAdapter(process.env.HERMES_API_KEY!),
});
// Envio de e-mail usando templates MJML gerenciados no painel
await hermes.email()
.to('cliente@email.com')
.subject('Confirmação de Acesso')
.useTemplate('cltmpl_cadastro_usuario', {
nome: 'Maria',
codigo: '482910'
})
.send();
6. Tradeoffs e Limitações Conhecidas
Uma análise técnica sincera exige pontuar as desvantagens e os limites da arquitetura:
- Custo de Manutenção de Infraestrutura:
O Hermes exige manter Node.js, PostgreSQL, Redis e containers de workers. Para projetos pequenos que disparam menos de 100 e-mails por mês, serviços gerenciados (como Resend ou Postmark) trazem menos sobrecarga operacional. O Hermes faz sentido quando há necessidade de soberania de dados, multi-tenancy interno ou conexão com infraestruturas SMTP legadas/próprias. - Custo de CPU do Argon2id sob Carga Alta:
Mesmo com o prefixo reduzindo a busca a , o cálculo do Argon2id é intencionalmente intensivo em memória e processamento. Picos extremos de requisições (>500 req/s simultâneas diretamente na API) elevam consideravelmente o uso de CPU. Para cargas muito altas, é recomendável manter uma camada de rate limiting ou proxy reverso na frente. - Escalonador Atrelado ao Docker Socket:
O módulo de auto-scaling de workers incluído (scaler.ts) inspeciona o volume de jobs no Redis e ajusta o número de réplicas via CLI do Docker Compose local. Esse formato atende bem VPSs únicas, mas não substitui orquestradores distribuídos (como HPA em Kubernetes). - Reputação de IP e Entregabilidade:
O Hermes resolve fila, templates, segurança e auditoria, mas não gerencia reputação de IP nem configurações de DNS (SPF, DKIM, DMARC), que continuam sob responsabilidade da configuração do servidor SMTP apontado.
7. Código Aberto e Contribuições
O projeto foi estruturado com foco em boas práticas de TypeScript, Drizzle ORM e isolamento de processos:
- API & Workers: hermes-api no GitHub
- Frontend Next.js: hermes-front no GitHub
- SDK Node/TypeScript: hermes-client no GitHub e no NPM
Feedbacks sobre a arquitetura e sugestões de melhoria são sempre bem-vindos!