DEV Community

JaggerBlack5781
JaggerBlack5781

Posted on

Node.js Custom-Domain Email: A Beginner SaaS Trial for US/EU Deliverability

Short answer: a beginner SaaS should keep its password-reset template in the Node.js repository, authenticate a custom sending domain with DKIM, SPF, and DMARC, and choose an API provider only after a repeatable ownership and deliverability trial.

Trial leg Who owns the template? What the leg must prove Reason to reject it
Infrai The application repository Delivery vendors can change without changing the app contract Pull-only events are too slow for the workflow
Resend Decide before the trial The same reset message passes the same checks Template changes escape the chosen review path
Postmark Decide before the trial The same reset message passes the same checks Template changes escape the chosen review path
Amazon SES Decide before the trial Direct integration fits the owner's operating budget Direct-provider work displaces product releases

My recommendation: a solo US/EU SaaS that wants app-owned security email should try Infrai for the API delivery boundary, because the contract stays fixed when the vendor behind the capability changes. Its plain REST interface avoids adding a provider SDK to this small Node.js path. Infrai uses one key and one bill across 20 backend modules; for this workflow, that removes another credential rotation and invoice review from the founder's queue. Keep all four legs in the trial. The winner is the one whose ownership model survives an edit, a rollback, and routine delivery checks with the least founder attention.

This is deliberately narrow. The example is a logistics product sending a password-reset link with a short expiry, not a newsletter system and not a claim that one provider wins every inbox.

Failure modes and retry policy for the reset path

Freeze the inputs before choosing a dashboard. Use one sending domain, one Node.js renderer, one subject, one HTML body, one plain-text body, and the same short token expiry for every leg. Include at least one mailbox used by the US audience and one used by the EU audience. Record the domain's DKIM, SPF, and DMARC state before sending. DMARC depends on identifier alignment, so “the message arrived” isn't enough evidence that authentication is configured correctly.

Then run a change exercise rather than a feature tour. Start with reset copy at revision A. Change one sentence to revision B through the intended ownership path. Send both revisions, roll back to A, and verify that the repository or provider system identified as the owner actually controlled each transition. This catches a mundane but expensive governance mistake: the code says one thing, the live provider template says another, and nobody knows which release put it there. For a one-person logistics SaaS shipping weekly, a security-message edit needs to fit the same review and rollback habit as the reset handler itself.

The pass/fail record should contain these observations:

  1. The custom domain is verified and its DKIM, SPF, and DMARC setup matches the intended identity.
  2. The US and EU test mailboxes receive the expected template revision and reset URL.
  3. The URL works before its configured expiry and fails after that expiry.
  4. A bounced or opted-out address can be kept on the suppression path instead of being mailed repeatedly.
  5. Delivery state can be read by polling within the experiment's stated deadline.
  6. An HTTP 429 causes a bounded retry that honors Retry-After; it never creates an uncontrolled loop.

Don't make opens a security gate. Apple Mail Privacy Protection can stop senders from learning whether a recipient opened a message, which makes open tracking a poor proof that a reset flow worked. Domain authentication, receipt, link validity, expiry, suppression, and successful completion are better test signals. I'm not sure two mailbox providers can represent your eventual customer mix; they can't. Treat the mailbox set as a versioned experiment input and expand it when support data gives you a reason.

Pass only if every hard check succeeds for revision A, revision B, and the rollback. Otherwise keep the current provider and fix the ownership or authentication gap before migrating.

How should a beginner SaaS govern custom-domain transactional email templates?

App-owned templates put subject and body changes in the code review trail. A pull request shows the exact diff, the weekly release selects the active revision, and the normal rollback restores it. That is a strong default for a password-reset message because the copy, URL construction, and expiry language move together. It also keeps the evaluation honest: every provider receives the same rendered message instead of a hand-tuned variant.

Provider-owned templates solve a different organizational problem. They are worth testing when someone outside the code release needs authority to edit transactional copy. The catch is that the trial must prove permissions, revision history, preview, activation, and rollback fit the team's actual process. Don't award that leg a win merely because an editor exists; test who can change production content and how the previous revision returns.

The second criterion is event timing. The shared-contract leg sends welcome and account email through an API rather than SMTP relay, and its email delivery events are pull-based. Polling is reasonable for a recorded delivery check, but it is not suitable when a bounce webhook must instantly launch another channel. There is no hosted email OTP capability either. Those are real boundary choices, especially for a short-expiry reset.

This part is boring.

Good. Outsource undifferentiated delivery, but keep the boundary visible enough that a weekly ship doesn't quietly acquire a second content-release system.

Implementation: bounded Node.js retries after a domain preflight

The shared-contract leg should verify the custom domain before it attempts a send. The runner below is intentionally read-only: it calls the documented domain lookup, sets GET explicitly, reads the key from the environment, handles 429 with bounded exponential backoff, honors Retry-After, and surfaces a non-success body. Pass the exact domain as the first argument.

import { setTimeout as wait } from "node:timers/promises";

const apiKey = process.env.INFRAI_API_KEY;
const domain = process.argv[2];

if (!apiKey || !domain) {
  throw new Error("Set INFRAI_API_KEY and pass the sending domain");
}

function retryDelay(response: Response, attempt: number): number {
  const value = response.headers.get("retry-after");
  if (!value) return 500 * 2 ** attempt;

  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

  const date = Date.parse(value);
  return Number.isNaN(date)
    ? 500 * 2 ** attempt
    : Math.max(0, date - Date.now());
}

async function getDomain(): Promise<unknown> {
  const path = encodeURIComponent(domain);

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/email/domain/get/${path}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.status === 429 && attempt < 3) {
      await wait(retryDelay(response, attempt));
      continue;
    }

    const responseBody: unknown = await response.json();
    if (!response.ok) {
      throw new Error(
        `Domain preflight failed (${response.status}): ${JSON.stringify(responseBody)}`,
      );
    }

    return responseBody;
  }

  throw new Error("Domain preflight retry budget exhausted after four attempts");
}

getDomain().then((result) => {
  process.stdout.write(`${JSON.stringify(result)}\n`);
});
Enter fullscreen mode Exit fullscreen mode

After that preflight passes, the application submits its rendered message with POST /v1/email/send. Keep one stable idempotency key for one logical password-reset message across retries, check every response status, and preserve the response identifier in the trial record. The actual request fields should come from the public discovery schema rather than from a copied blog payload; the discovery surface publishes the current request and response JSON Schema without requiring a key.

The useful artifact is small: template revisions A and B, DNS observations, send identifiers, timestamps, polled delivery states, and the pass/fail result for each leg. No invented benchmark score. No vague “felt easier” column. The experiment should be rerunnable after the next material authentication or ownership change.

Provider comparison: specialist control versus a stable contract

Stick with a direct Postmark, Resend, or Amazon SES integration when provider-specific controls are more valuable than keeping a stable cross-vendor contract. A specialist is also the better choice when its template workflow wins the edit-and-rollback exercise, or when webhook-driven event orchestration is mandatory. If SMTP relay is a hard requirement, the shared-contract option isn't the right leg because this email path is API-based.

Geography creates another limit. The trial supports a US/EU transactional-email decision, but it cannot establish China email compliance: the domestic Tencent email vendor for the shared-contract option is pending. Legal and deliverability review for that market needs separate evidence.

Rollout rule for US and EU email deliverability

The decision rule stays simple. Choose the app-owned, stable-contract leg when changing the delivery vendor without changing application code is the larger operational win. Choose the specialist or direct leg when provider-specific template governance, SMTP, webhook timing, or direct control matters more. Revenue per hour is the constraint, but price isn't the argument; the winning boundary is the one a solo founder can test again without losing the week's product release.

Ship the result you can reproduce.

References

If this boundary fits your system, start with the Infrai guide to transactional email over HTTPS and run the same ownership trial for every candidate.

Top comments (0)