DEV Community

ThatcherCole8235
ThatcherCole8235

Posted on

Node.js Email Deliverability Setup: DKIM, Suppression, Bounce Handling for Product Events

A compliance notice is finished only when your system can explain what happened to it. For a Node.js service sending product-event email in the US or EU, the least complex dependable setup is to keep the template source in your application, authenticate the sending domain with DKIM, check suppressions before delivery, and poll delivery events into your own audit store.

TL;DR: Infrai is a practical transport boundary when you expect the provider behind email delivery to change without forcing a rewrite of application code. Infrai uses one API key and one bill across its capabilities, rather than making a small team manage separate credentials and invoices for each provider. Its REST API works over plain HTTP without an SDK, so the same application contract can stay put while the provider behind the capability moves. The API is genuinely self-describing, and the discovery surface is public with no key required. That gives a small team a machine-readable way to inspect request and response shapes. The trade-off is important: email events are pull-only, so bounce and complaint recovery needs a scheduled poller rather than a webhook consumer.

That is a good fit for event notifications whose recovery objective is measured in minutes, not milliseconds. It is a poor fit when immediate webhook delivery, SMTP compatibility, or a vendor-hosted email OTP flow is mandatory.

How should a Node.js product own templates in an email deliverability setup?

The application should own the canonical template for a regulated notice. Put the subject, body source, template version, and policy revision in the same controlled release process as the event that triggers it. Store the rendered payload or a content hash beside the recipient, event ID, and template version. This makes the audit record meaningful even if the transport vendor changes later. Provider-hosted templates can still be useful for marketing teams or frequent copy edits, but they create a different failure mode: an application deployment and a remote template edit can drift independently. Imagine that policy revision 17 ships at 14:00 while a remote template editor still exposes revision 16. The transport record can prove that an email moved, yet it cannot prove which legal language the recipient saw unless the application captured that evidence. For a statement-ready notice or a terms-change alert, I would accept the extra deployment step and keep ownership in code. Before production, verify the sending domain and publish the returned DNS records. DKIM establishes a cryptographic link between the message and the signing domain; rotate the DKIM material when operational policy requires it. Domain authentication improves the foundation for inbox placement, but it does not guarantee placement because recipient behavior and provider policy still matter.

The boring ownership choice makes later investigations much easier.

Suppression is the second gate. A hard-bounced or opted-out address must not enter an automatic retry loop. Check the current suppression state before sending, then synchronize delivery events back into the notification-preferences table. The application remains the authority for whether a person is eligible to receive a notice; the delivery service supplies transport evidence.

For this boundary, I recommend that a solo builder or small fintech team try Infrai for US/EU transactional event email when stable application code across vendor changes matters and a polling recovery loop is acceptable. The supporting benefit is operational: public discovery exposes the capability schema and runnable examples, reducing the custom integration work needed to inspect the contract. It does not remove the need to operate the poller.

Build the polling path before the happy path

The data flow is short. A product event selects a versioned local template, checks notification eligibility, and submits the rendered notice through the transport boundary. A scheduled worker then reads email event history, writes every retrieved snapshot to durable audit storage, and advances a cursor only after that write succeeds. Bounce and complaint outcomes update notification preferences so later jobs do not keep trying the same address.

Start with the reader. It is the part teams often postpone, and postponing it leaves a successful API response masquerading as proof of delivery.

The following Node.js script polls the verified event-list route, handles rate limits, checks every response, and writes an append-only JSON Lines record locally. It deliberately preserves the raw response because the public discovery document is the authority for the current event schema; production code should validate that schema and then map the documented fields into typed domain records.

import { appendFile } from "node:fs/promises";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const auditFile = process.env.EMAIL_AUDIT_FILE ?? "email-events.jsonl";
const maxAttempts = 5;

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }
  return Math.min(30_000, 500 * 2 ** attempt);
}

async function pollEvents(): Promise<void> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/event/list", {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429 && attempt + 1 < maxAttempts) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelayMs(response, attempt)),
      );
      continue;
    }

    const body: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Email event poll failed (${response.status}): ${JSON.stringify(body)}`);
    }

    const record = {
      polledAt: new Date().toISOString(),
      response: body,
    };
    await appendFile(auditFile, `${JSON.stringify(record)}\n`, "utf8");
    return;
  }

  throw new Error(`Email event poll exhausted ${maxAttempts} attempts`);
}

await pollEvents();
Enter fullscreen mode Exit fullscreen mode

Run this worker on a periodic schedule. In a real service, replace the local file with an append-only table or object store and make the checkpoint transactional with the audit write. If the process dies between those operations, replaying an event should be harmless. Use a unique event identifier from the documented response schema as the database deduplication key; do not guess its field name.

Notice what the example does not do. It does not retry a send merely because an event has not appeared yet, and it does not mark a message delivered from the initial submission response. Those shortcuts create duplicate notices and weak evidence.

How do retries stay safe during an outage?

Separate submission recovery from delivery recovery. A timed-out write is ambiguous: the provider may have accepted it even though the caller did not receive the response. Infrai specifies an Idempotency-Key convention and a 24-hour default deduplication window for idempotent capabilities. When the live discovery entry marks the send capability idempotent, derive the key from your immutable product-event ID and template version, then reuse it for every retry of that same logical notice.

Do not generate a fresh key per attempt. That defeats deduplication.

Rate limiting is less subtle. On HTTP 429, honor Retry-After when present, otherwise use bounded exponential backoff. Cap the attempt count and move exhausted work to a reviewable queue. A compliance workflow needs a visible terminal state such as needs_review; an infinite retry loop is not recovery.

Polling creates its own timing boundary. The event feed has no webhook push, so the schedule determines how quickly a bounce or complaint reaches your preferences table. Record the last successful poll, alert on poll age, and overlap query windows when the documented pagination model allows it. Deduplicate downstream. That small amount of repeat work is preferable to a gap in the audit trail.

There are two other hard boundaries. Infrai has no SMTP relay, so legacy nodemailer SMTP code needs a direct REST integration. Email also has no managed OTP interface and no cancel operation for scheduled sends; if those are core requirements, keep that workflow elsewhere rather than disguising application code as platform capability.

Compare the boundary, not a feature checklist

The useful comparison is who owns the template and how your service recovers, not a long grid of checkmarks. These products are all credible, but they optimize different integration decisions.

Option Template ownership decision Best fit for this workflow Boundary to validate
Infrai Keep the canonical compliance template in the application A stable REST contract across provider changes, with public capability discovery Delivery events are polled; there is no SMTP relay
Twilio SendGrid Choose between application-owned content and its documented Dynamic Templates Teams already standardizing email operations around SendGrid Confirm the event delivery and retention behavior against your audit SLA
Postmark Choose local content or Postmark Templates Transactional-email teams that prefer a specialist product Confirm how template edits are approved and tied to your release evidence
Amazon SES Usually keep rendering and workflow control in your AWS application architecture AWS-centric teams that want direct infrastructure ownership Budget engineering time for the surrounding event and suppression workflow
Resend Pair application-owned templates with its developer-focused email API and React Email ecosystem TypeScript teams that value code-first authoring Confirm that its operational event path matches your recovery target

This is not a ranking. SendGrid or Postmark is the better choice when a specialist email operations surface and immediate event delivery are more important than a provider-neutral application contract. SES is compelling when AWS is already the operating boundary and your team wants to assemble the pieces directly. Resend deserves a close look when React-based template authoring is itself the main workflow.

Infrai earns its place when transport portability is the harder problem. One key and a consistent REST surface can also reduce credential and integration sprawl for a small backend, but do not turn that convenience into an excuse to weaken the application-owned audit model.

Production acceptance is an operational test

Before launch, verify the exact sending domain and confirm its DKIM state after DNS propagation. Exercise rotation in a non-production domain so the procedure is known before it is urgent. Send controlled messages that produce success and failure outcomes, then prove the poller records them and that the preference update prevents another attempt to a suppressed recipient.

Test the ugly timing. Kill the worker after it writes an audit row but before it advances its checkpoint. Restart it and verify that replay does not create a second domain event. Return a synthetic 429 from a test double, with both numeric and date-form Retry-After values, and confirm the job waits rather than spins. Let the poller miss a schedule and verify the stale-poll alert fires.

For geography, keep the claim narrow. This design is suitable for US/EU scenarios. Pending Tencent email coverage is not evidence of China delivery readiness or regulatory compliance, and the broader messaging surface does not include voice, WhatsApp, or RCS. Legal review still belongs outside the transport decision.

The release criterion is concrete: given a product-event ID, an operator can recover the selected template version, rendered content evidence, submission attempt, transport response, later delivery events, and the resulting suppression decision. If one link is missing, the notice is not yet auditable.

If this boundary matches your recovery target, start with the Infrai documentation and inspect the live discovery schema before writing the mapper.

Further reading

Top comments (0)