DEV Community

ValerianBlack3895
ValerianBlack3895

Posted on

Email Event Notifications vs Managed Relay — Choose by 3 Integration Tests

Short answer: To implement email event notifications in Node.js, use a managed relay behind an application-owned audit log when integration effort is the main constraint; choose direct delivery only when mail operations are already a maintained capability.

A managed relay is the least complex choice for a B2B SaaS compliance notice when the requirement is an auditable delivery record and the team needs to ship weekly. Choose direct delivery only when you already operate mail infrastructure and can own sender reputation, authentication, retries, and evidence retention.

Decision point Direct delivery Managed relay
Integration effort Higher: mail transport, reputation, feedback loops, and DNS are yours Lower: your app calls a bounded notification interface
Audit record You design the event log and provider responses You still design the event log; the relay is not the system of record
Best fit A team with existing mail operations A small product team optimizing revenue per engineering hour

My default is the managed relay, with an application-owned audit log. That decision is about integration effort, not a claim that one delivery path is universally better. The relay should remove undifferentiated mail operations while your service keeps ownership of what was requested, when it was attempted, and what acknowledgement came back.

The ledger stays boring.

What must an auditable notification prove?

A compliance notice is an event, not a call to an email API. The record needs a stable event ID, the recipient or recipient reference, the template revision, the requested time, the attempt count, and the final state. Keep the payload small and avoid copying sensitive content into logs. Store a content hash if reviewers need to verify which rendered message was sent.

This is where many implementations become hard to defend. A successful HTTP response proves that a downstream system accepted a request. It does not prove inbox placement, human reading, or even permanent delivery. Model those states separately. requested, accepted, delivered, bounced, and failed are useful application states, but only use a state when the event source supports that claim.

The notification worker should be idempotent. Give each event a deterministic key, persist it before dispatch, and make a retry of the same key harmless. A timeout is ambiguous: the request may have reached the relay even though your process did not receive the response. Retrying with a new message ID can create duplicate compliance notices.

I initially thought the audit table was the easy part. The awkward detail is the boundary between an attempt and an outcome. An attempt is yours. A delivery outcome is evidence received later, often through a webhook or polling process. Treating them as one row with one mutable status makes incident review harder.

Here is the practical test. Suppose a customer disputes a notice sent on 2026-10-28. The reviewer should be able to start with the business event, find the template version and recipient reference, see the first request and any retry under the same event ID, then inspect the later delivery signal without confusing it with the original acceptance. If the worker only kept a provider message ID, the team may know that something was sent but still lack the chain that explains what happened. The audit record is useful because it preserves that chain, including an unknown outcome that needs follow-up rather than quietly becoming a failure.

That is the boundary.

The limitation is real: outsourcing transport can reduce integration effort while reducing control over the sending boundary. That trade-off belongs in the decision record.

Which integration path fits a one-person SaaS?

Use three tests before choosing. First, can the team explain who owns DNS authentication, bounce handling, suppression, and reputation? Second, can a notification be replayed without duplicating the customer-facing event? Third, can an auditor trace one notice from business event to request, attempt, provider acknowledgement, and later delivery signal?

If the answer to the first test is already part of normal operations, direct delivery may be reasonable. It gives the team control over the transport boundary, but the cost is ongoing operational work. SPF is one piece of that boundary: RFC 7208 defines a mechanism for publishing and evaluating authorized sending hosts, not a complete proof of delivery. DKIM and DMARC belong in the wider authentication design, and none of these standards replaces an application audit trail.

A managed relay is the better fit when the product team has one engineer and the alternative is delaying a feature. It should expose enough status information for your audit model, support stable request identifiers or idempotency behavior, and provide exportable event evidence. Those are acceptance criteria. They are not reasons to outsource the business record.

The distinction matters because the same boundary appears across products. Amazon SES centers on a sending service and configuration sets for event publishing. SendGrid documents event webhooks for delivery and engagement signals. Mailgun documents event data and routes for mail processing. Their interfaces and operational defaults differ, so compare the exact evidence and retry semantics you need instead of treating a familiar brand as an architecture.

How do I implement email event notifications in Node.js with code?

Keep the application-facing interface generic. The worker can call an adapter for direct SMTP delivery or a relay, while the audit code remains unchanged. The example below uses Node.js fetch against a pseudonymous endpoint; the endpoint contract is intentionally yours to define.

type Notification = {
  eventId: string;
  recipient: string;
  template: string;
  templateVersion: string;
  payload: Record<string, unknown>;
};

type DispatchResult = {
  accepted: boolean;
  providerMessageId?: string;
};

async function dispatchEmail(notification: Notification): Promise<DispatchResult> {
  const response = await fetch("https://mail.example.test/v1/email/batch/send", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "idempotency-key": notification.eventId,
    },
    body: JSON.stringify({
      to: notification.recipient,
      template: notification.template,
      templateVersion: notification.templateVersion,
      variables: notification.payload,
    }),
  });

  if (!response.ok) {
    throw new Error(`notification dispatch failed: ${response.status}`);
  }

  const result = (await response.json()) as { messageId?: string };
  return {
    accepted: true,
    providerMessageId: result.messageId,
  };
}
Enter fullscreen mode Exit fullscreen mode

The adapter records an attempt before the call and records the acknowledgement after it. On a timeout, mark the attempt as unknown and retry with the same idempotency key according to a bounded policy. Do not mark it failed merely because your socket closed. Also make the worker's queue visible: age of the oldest pending event, retry count, terminal failures, and the rate of unknown outcomes are more useful than a single success percentage.

A real implementation needs a secret manager, input validation, suppression handling, and a dead-letter path. It also needs tests for duplicate delivery, an expired template, a malformed address, a 429 response, and a response that arrives after the client timeout. Those tests are cheap compared with reconstructing an audit trail after a customer asks for it.

A managed relay is a poor fit if its event evidence cannot be exported, its retry behavior is opaque, or its identifiers cannot be joined to your business event. Direct delivery is also a poor fit for a one-person SaaS if it turns a weekly feature into permanent mail operations. I would reject either path on those grounds, even if its initial integration looked short.

When direct delivery is the better choice

Direct delivery wins when mail transport is already a maintained capability, regulatory boundaries require control over the sending path, or the message volume and feedback model justify dedicated operations. The trade is predictable: more control creates more surface area. You own DNS changes, key rotation, reputation monitoring, bounce classification, and the evidence pipeline.

For a small SaaS, the decision rule is simple. Pick the path that passes the three integration tests with fewer permanent responsibilities. Keep the event ledger in your database either way. That preserves the compliance boundary while leaving the transport choice replaceable.

Further reading

Top comments (0)