1

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 O(1)O(1) 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:


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:

  1. Latência no cliente: O usuário final fica com o botão travado esperando a confirmação da rede externa de e-mail.
  2. 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.
  3. 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 Created contendo 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:

  1. Resgata a credencial de envio do tenant (senha SMTP pura ou token dinâmico via Google OAuth2/Gmail API).
  2. Descriptografa a credencial em memória.
  3. Compila o template MJML injetando as variáveis dinâmicas com Handlebars.
  4. Dispara a mensagem via Nodemailer.
  5. Em caso de sucesso ou falha definitiva, atualiza o status no PostgreSQL (sent ou failed) com stack de erros e timestamp.
  6. 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(N)O(N)), 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)
  1. Busca Indexada (O(1)O(1)): 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.
  2. Validação Forte: Com a linha recuperada, o sistema aplica o argon2.verify(key_hash, secret) exclusivamente no segredo recebido.
  3. 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:

  1. 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).
  2. O sistema gera a nova chave candidata e envia uma requisição POST HTTPS para o endpoint de webhook configurado no serviço do cliente.
  3. 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');
    
  4. 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.
  5. Atualização Efetiva: Apenas quando a aplicação cliente confirma com 200 OK o 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:

  1. 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.
  2. Custo de CPU do Argon2id sob Carga Alta:
    Mesmo com o prefixo reduzindo a busca a O(1)O(1), 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.
  3. 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).
  4. 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:

Feedbacks sobre a arquitetura e sugestões de melhoria são sempre bem-vindos!

Carregando publicação patrocinada...