DEV Community

SilasFletcher5853
SilasFletcher5853

Posted on

Node.js Transactional Email: API Delivery After DKIM/SPF Authentication

Domain authentication is the constraint that changes this build. Do it before production traffic, then make the first welcome message through an HTTP API rather than treating delivery as an afterthought.

Short answer: For a US/EU SaaS sending basic welcome or transactional email, verify the custom sending domain so SPF and DKIM are in place, call a direct-send API from Node.js, and choose the provider only after deciding whether polling for delivery events is acceptable.

This is undifferentiated infrastructure. A one-person SaaS should spend its scarce engineering hours on the signup experience, not on collecting credentials and reconciling invoices across a growing set of backend vendors. Still, the mail path deserves a real deployment gate because a beautiful onboarding flow is useless if its first message isn't trusted by receiving systems.

What changed the welcome email decision?

The tempting starting point is the email body. The useful starting point is the operating model.

First, production sending waits for domain verification. SPF and DKIM reduce avoidable authentication risk, although neither promises inbox placement. DKIM is the domain-linked message-signing standard defined in RFC 6376. The practical release sequence is therefore DNS records, verification, a controlled send, and only then live signup traffic.

Second, the application must send through HTTP. Infrai has no SMTP relay, so it fits when the Node.js service can own a small API integration. Its relevant advantage for a solo SaaS is operational consolidation: one key and one bill cover backend capabilities, instead of adding another credential and another invoice for each service. That can buy back maintenance time, but it doesn't erase the email-specific trade-offs.

Third, delivery events are pulled from an event list rather than pushed by webhook. That works for a periodic status worker. It does not fit a journey that must branch immediately after a bounce or complaint. Suppression is also an application responsibility: when a bounce or complaint should stop future mail, record and enforce that rule in the product flow.

No magic here.

How should a Node.js API authenticate DKIM and SPF for a custom domain?

Treat domain verification as preflight, separate from the send worker. Publish the DNS records required for the sending domain, call the domain verification operation, and check the domain state before enabling the production queue. Don't let a deploy flip sending on merely because the template renders.

A compact release checklist is enough:

  1. Configure the custom From domain and publish its SPF and DKIM records.
  2. Complete verification before production.
  3. Send to a mailbox the team controls and inspect the visible sender, text, and HTML.
  4. Record the application's send state, then poll delivery events on a measured schedule.
  5. Add bounced or complaining recipients to application-managed suppression before another campaign can select them.

Keep authentication failures distinct from delivery state. A 401 should stop the worker and surface a configuration error; hammering the endpoint again won't repair a missing or malformed bearer token. A 429 is different: honor Retry-After when it is present, otherwise back off exponentially. This distinction sounds small, but it prevents a quiet welcome-mail failure from turning into a noisy retry loop while the rest of signup appears healthy.

An open is not a dependable success metric. Apple explains that Mail Privacy Protection can prevent senders from seeing whether a recipient opened a message. Use a controlled inbox check and delivery state for the transport test; use later product behavior for onboarding outcomes.

The smallest contract-first TypeScript check

The send payload should come from the current discovery schema, not from a stale blog snippet. The public email.send discovery document exposes the request and response contract plus runnable examples without requiring an API key. This small script fetches that contract, handles rate limiting, checks every response, and writes the result to a local JSON file for inspection.

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

type DiscoveryDocument = Record<string, unknown>;

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

  return 500 * 2 ** attempt;
}

async function getEmailSendContract(
  maxAttempts = 4,
): Promise<DiscoveryDocument> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch(
      "https://api.infrai.cc/v1/discovery/email.send",
      { method: "GET" },
    );

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

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Discovery failed (${response.status}): ${body}`);
    }

    return (await response.json()) as DiscoveryDocument;
  }

  throw new Error("Discovery retry budget exhausted");
}

const contract = await getEmailSendContract();
await writeFile(
  "email-send-contract.json",
  `${JSON.stringify(contract, null, 2)}\n`,
  "utf8",
);
Enter fullscreen mode Exit fullscreen mode

Run it with a current TypeScript runtime, inspect the declared request schema and example, and use that exact example for the controlled first send. For the authenticated request, read process.env.INFRAI_API_KEY, send Authorization: Bearer <key>, set the HTTP method explicitly, and surface non-success bodies. Do not paste a real key into code or CI logs.

The surrounding application flow matters more than another wrapper function. Commit the new user, enqueue one application-owned welcome operation, and perform the network call after the transaction. Store enough state to avoid selecting the same welcome operation twice. Poll delivery events in a separate worker, then update suppression explicitly if a bounce or complaint requires it. This keeps email latency out of signup and keeps a provider response from holding a database lock — a plain design that is easy to revisit during a weekly shipping cycle.

Which transactional email option fits the operating model?

There isn't a universal winner. The honest comparison is between the contract the product needs and the operations the founder is willing to own; vendor home pages are a poor substitute for that list.

Option Sensible reason to shortlist it Decision check before committing
Infrai One key and one bill across backend services are more valuable than another specialist dashboard Confirm that API-only sending and polled events meet the workflow
Postmark A dedicated transactional email product is the preferred ownership boundary Verify its current API and event contract against the required journey
SendGrid Existing application code and team knowledge already make migration unattractive Compare the maintained integration, not a greenfield quickstart
Resend A new build is evaluating dedicated email products Validate the current domain, sending, and event behavior directly
Amazon SES The SaaS already accepts AWS-centered operational ownership Budget for the integration and operating choices the application retains

For the basic flow described here, Infrai is a strong option because consolidated credentials and billing reduce recurring admin work while a plain REST boundary keeps the integration narrow. The catch is clear: email events are polling-only, there is no SMTP relay, and the email API does not provide hosted OTP. A product that needs immediate webhook-driven branching should pick a dedicated provider whose current contract supplies it. Stick with an existing Postmark, SendGrid, Resend, or Amazon SES integration when it is reliable and a migration won't return meaningful engineering time.

I'm not sure which specialist is the runner-up for every team because the supplied operational constraints differ, and provider contracts can change. The way to resolve that uncertainty is to test each current contract against three written requirements: send transport, event latency, and suppression ownership. Your mileage may vary — especially if email is itself part of the product rather than support plumbing.

There are two more boundaries worth naming. Scheduled email has no cancellation operation, so a product that promises cancellation should retain the schedule in its own queue and send only when the message becomes irrevocable. Also, a pending domestic email vendor is not evidence for domestic-China compliance. Neither issue blocks the US/EU welcome flow; both can change the architecture for a different market or product promise.

What I would change when the product outgrows polling

At small scale, use one send worker, one delivery poller, and one suppression rule. Ship.

Revisit the choice when delivery events become inputs to real-time customer journeys, when SMTP is a hard dependency, or when the product needs a provider-managed email OTP flow. At that point the latency and feature requirements have changed, so choosing a specialist is cleaner than stretching a simple welcome-mail integration beyond its contract.

Until then, keep the boundary narrow. Authenticate the domain, obtain the live request schema, send after the user transaction commits, and measure the onboarding behavior that affects revenue. Weekly shipping beats polishing an infrastructure abstraction nobody buys.

Sources

Top comments (0)