<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruan Lopes</title>
    <description>The latest articles on DEV Community by Ruan Lopes (@ruan_lopes_ff1139db227941).</description>
    <link>https://dev.to/ruan_lopes_ff1139db227941</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4132012%2Febf0ecd2-34ab-4dd5-8049-b653c725f6d3.png</url>
      <title>DEV Community: Ruan Lopes</title>
      <link>https://dev.to/ruan_lopes_ff1139db227941</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ruan_lopes_ff1139db227941"/>
    <language>en</language>
    <item>
      <title>Engineering a Self-Hosted Transactional Email Gateway: Architecture, Async Queues, and Cryptographic Key Rotation</title>
      <dc:creator>Ruan Lopes</dc:creator>
      <pubDate>Fri, 18 Sep 2026 19:37:08 +0000</pubDate>
      <link>https://dev.to/ruan_lopes_ff1139db227941/engineering-a-self-hosted-transactional-email-gateway-architecture-async-queues-and-4cek</link>
      <guid>https://dev.to/ruan_lopes_ff1139db227941/engineering-a-self-hosted-transactional-email-gateway-architecture-async-queues-and-4cek</guid>
      <description>&lt;p&gt;Connecting web applications directly to SMTP servers inside request handlers introduces unpredictable latency, connection overhead, and fragmented credential management across microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hermes&lt;/strong&gt; is an open-source, multi-tenant transactional email gateway designed to solve this problem by decoupling request intake from actual delivery using a BullMQ/Redis queue. The system secures credentials at rest using &lt;strong&gt;AES-256-GCM&lt;/strong&gt; and validates high-frequency programmatic API keys using &lt;strong&gt;Argon2id&lt;/strong&gt; with indexed prefix lookups to avoid full-table scans.&lt;/p&gt;

&lt;p&gt;The codebase is organized into modular repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway &amp;amp; Workers:&lt;/strong&gt; &lt;a href="https://github.com/RuanLopes1350/hermes-api" rel="noopener noreferrer"&gt;github.com/RuanLopes1350/hermes-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend Management Dashboard:&lt;/strong&gt; &lt;a href="https://github.com/RuanLopes1350/hermes-front" rel="noopener noreferrer"&gt;github.com/RuanLopes1350/hermes-front&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript Client SDK:&lt;/strong&gt; &lt;a href="https://github.com/RuanLopes1350/hermes-client" rel="noopener noreferrer"&gt;github.com/RuanLopes1350/hermes-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPM Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@ruanlopes1350/hermes-client" rel="noopener noreferrer"&gt;&lt;code&gt;@ruanlopes1350/hermes-client&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The Core Architecture
&lt;/h2&gt;

&lt;p&gt;In distributed architectures, transactional emails (receipts, password resets, system alerts) should not block the user-facing request cycle. SMTP handshakes, TLS negotiation, and remote server response times frequently take between 500ms and 3,000ms. If an SMTP server stalls, threads hang and upstream requests timeout.&lt;/p&gt;

&lt;p&gt;Hermes separates the ingestion interface from the transport layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Client App / SDK ]
         │
         │  POST /api/emails (X-API-Key: hm_prefix.secret)
         ▼
┌─────────────────────────────────────────────────────────────┐
│                    HERMES API GATEWAY                       │
│  - Validates API Key (Prefix index lookup + Argon2id verify)│
│  - Records email in Postgres as 'pending'                   │
│  - Pushes job to BullMQ queue (&amp;lt; 25ms response time)        │
└──────────────┬──────────────────────────────┬───────────────┘
               │                              │
               ▼                              ▼
     ┌────────────────────┐         ┌───────────────────┐
     │  PostgreSQL DB     │         │   Redis (BullMQ)  │
     │  (Drizzle ORM)     │         │   (Queue/PubSub)  │
     └────────────────────┘         └─────────┬─────────┘
                                              │
                                              ▼
                                ┌───────────────────────────┐
                                │       EMAIL WORKER        │
                                │  - Decrypts AES-256 keys  │
                                │  - Renders MJML template  │
                                │  - Dispatches via SMTP    │
                                │  - Updates DB &amp;amp; fires SSE │
                                └─────────────┬─────────────┘
                                              │
                                              ▼
                                ┌───────────────────────────┐
                                │     SMTP SERVER / GMAIL   │
                                └───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Decoupling the Ingestion API from the Worker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ingestion Gateway (&lt;code&gt;server.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The API accepts incoming requests, performs schema validation, checks permissions, stores the email record in PostgreSQL as &lt;code&gt;pending&lt;/code&gt;, and pushes a job to Redis. It returns an immediate &lt;code&gt;201 Created&lt;/code&gt; with the email record ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fast response path: No SMTP handshakes inside HTTP execution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;recipientTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;send-email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;emailId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;emailRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CommonResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E-mail enfileirado com sucesso!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emailRecord&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Background Execution Worker (&lt;code&gt;worker.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The worker pulls jobs off the BullMQ queue asynchronously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolves the tenant's SMTP credentials (either username/password or Google OAuth2 refresh tokens).&lt;/li&gt;
&lt;li&gt;Decrypts sensitive fields in memory.&lt;/li&gt;
&lt;li&gt;Compiles the MJML template with dynamic Handlebars variables.&lt;/li&gt;
&lt;li&gt;Executes delivery via Nodemailer.&lt;/li&gt;
&lt;li&gt;Updates the database state to &lt;code&gt;sent&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; (capturing error stacks and execution timestamps).&lt;/li&gt;
&lt;li&gt;Publishes an event to Redis Pub/Sub, streaming real-time status updates to the dashboard via Server-Sent Events (SSE).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a network glitch or rate-limit occurs, BullMQ handles exponential backoff retries without blocking new HTTP requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Cryptographic Implementation
&lt;/h2&gt;

&lt;p&gt;A shared gateway must safely store external credentials and validate incoming API requests at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 API Key Verification: Prefix-Indexed Argon2id
&lt;/h3&gt;

&lt;p&gt;Hashing API keys with standard SHA-256 leaves them vulnerable to high-speed dictionary attacks if a database dump leaks. Conversely, hashing the entire incoming key with &lt;code&gt;bcrypt&lt;/code&gt; or &lt;code&gt;argon2id&lt;/code&gt; without indexing forces an $O(N)$ linear scan over all database records.&lt;/p&gt;

&lt;p&gt;Hermes solves this by splitting API keys into two components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hm_b5c92a10.e4d3c2b1a0f9e8d7c6b5a4938271605f45819027814a09823...
└───┬──────┘ └─────────────────────────┬─────────────────────────┘
    │                                  └─ 64-char Hex Secret (Argon2id Hash)
    └─ 8-char Hex Prefix (Indexed plaintext)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Index Lookup ($O(1)$):&lt;/strong&gt; The database indexes the public &lt;code&gt;prefix&lt;/code&gt; (&lt;code&gt;hm_b5c92a10&lt;/code&gt;). When an API call arrives, the query retrieves only the matching record:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; 
   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;credential&lt;/span&gt; 
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'hm_b5c92a10'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id Verification:&lt;/strong&gt; Once the candidate row is fetched, &lt;code&gt;argon2.verify(candidate.key_hash, secret)&lt;/code&gt; validates the secret. This gives memory-hard cryptographic protection against GPU brute-forcing while avoiding table scans.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.2 Credentials at Rest: AES-256-GCM Envelope Encryption
&lt;/h3&gt;

&lt;p&gt;Passwords and Google OAuth2 tokens are stored in the database formatted as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;iv_hex&amp;gt;:&amp;lt;auth_tag_hex&amp;gt;:&amp;lt;ciphertext_hex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; AES-256-GCM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IV:&lt;/strong&gt; Unique 16-byte initialization vector generated randomly for each encryption call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Tag:&lt;/strong&gt; 16-byte authentication tag ensuring ciphertext integrity (detecting data corruption or tampering).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Master Key:&lt;/strong&gt; Derived from the &lt;code&gt;MASTER_KEY&lt;/code&gt; environment variable, ensuring that database leaks alone do not expose usable SMTP credentials.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Zero-Downtime Key Rotation (Webhook-First Pattern)
&lt;/h2&gt;

&lt;p&gt;Rotating API keys in production usually requires manual orchestration to avoid downtime. Hermes automates rotation using an asynchronous cron job and signed webhooks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; A daily cron job (&lt;code&gt;0 0 * * *&lt;/code&gt;) scans active credentials where &lt;code&gt;auto_rotate = true&lt;/code&gt; and &lt;code&gt;expiresAt&lt;/code&gt; is within the configured threshold (default: 3 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook-First Dispatch:&lt;/strong&gt; The system generates a new key pair and attempts to deliver it to the service's configured webhook URL via HTTPS &lt;code&gt;POST&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMAC SHA-256 Verification:&lt;/strong&gt; The payload is signed with an &lt;code&gt;X-Hermes-Signature&lt;/code&gt; header calculated using the service's private &lt;code&gt;webhook_secret&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Failure Safety:&lt;/strong&gt; If the client application fails to acknowledge the webhook with a &lt;code&gt;200 OK&lt;/code&gt; (e.g., service unavailable or invalid signature), the rotation is rolled back. The database record is &lt;strong&gt;not&lt;/strong&gt; updated, and the existing key remains valid. BullMQ schedules a retry with exponential backoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Commit:&lt;/strong&gt; The new key hash and prefix are written to PostgreSQL only after the client acknowledges receipt.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Integrating with the SDK
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@ruanlopes1350/hermes-client&lt;/code&gt; package provides a fluent builder pattern, automatic retries with jitter, and built-in webhook handlers for key rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HermesClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MemoryAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ruanlopes1350/hermes-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hermes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HermesClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://hermes.internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storageAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HERMES_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Fluent email dispatch&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hermes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password Reset Request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cltmpl_password_reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dev User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="na"&gt;reset_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.internal/reset?token=xyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To handle key rotations automatically in Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expressWebhookHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ruanlopes1350/hermes-client/express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Requires raw body for HMAC verification&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/webhooks/hermes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nf"&gt;expressWebhookHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hermes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HERMES_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Tradeoffs and Limitations
&lt;/h2&gt;

&lt;p&gt;A transparent look at the architectural constraints of this setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure Footprint:&lt;/strong&gt; Unlike fully managed SaaS providers (Resend, SendGrid, Postmark), Hermes requires maintaining a Node.js runtime, PostgreSQL, Redis, and worker instances. For small applications sending a few dozen emails per week, the operational overhead may not be justified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id CPU Consumption:&lt;/strong&gt; Under high-concurrency spikes (&amp;gt;500 req/s), Argon2id verification puts heavy load on the CPU. While the prefix index eliminates full-table scans, verifying memory-hard hashes repeatedly remains compute-heavy. Deployments with extreme throughput should place a caching proxy or rate-limiter in front of the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-Host Worker Scaling:&lt;/strong&gt; The included auto-scaler (&lt;code&gt;scaler.ts&lt;/code&gt;) manages worker replica counts by communicating with the local Docker Compose daemon. It is designed for single-host VPS infrastructure, not distributed clusters (e.g., Kubernetes HPA).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliverability Responsibility:&lt;/strong&gt; Hermes handles queuing, template rendering, and delivery handoffs. It does not manage upstream IP reputation, feedback loops, or DNS records (SPF, DKIM, DMARC), which remain the responsibility of your underlying SMTP provider.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Project Repositories
&lt;/h2&gt;

&lt;p&gt;The complete source code is open for review and contributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/RuanLopes1350/hermes-api" rel="noopener noreferrer"&gt;Hermes API &amp;amp; Workers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RuanLopes1350/hermes-front" rel="noopener noreferrer"&gt;Hermes Admin Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RuanLopes1350/hermes-client" rel="noopener noreferrer"&gt;Hermes TypeScript Client (SDK)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@ruanlopes1350/hermes-client" rel="noopener noreferrer"&gt;NPM Package Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>node</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
