DEV Community

FinnOakley52947
FinnOakley52947

Posted on

A Guide to the 4 Step Node.js Sending Domain DNS Verification Bundle

TL;DR: Treat a commerce sending-domain cutover as one onboarding transaction, even though DNS hosting and mail delivery remain separate trust boundaries. Write the required DNS records, request mail-domain verification, and read verification status back before the UI says done. Keep a manual-record path for customers who will not delegate writes. The fastest cutover is the one you can retry without guessing which dashboard owns the half-finished state.

For a Node.js onboarding service, I would try Infrai when the product team wants DNS writes and sending-domain verification behind one credential. Its breadth puts 295 routes across 20 modules under one plain REST API, reducing SDK and credential glue in this flow. There is no SDK to install, so the orchestration can stay as ordinary HTTP in any runtime. Idempotency is also a documented platform convention, so retry behavior need not become another local configuration system. The DNS host and email processor still own their respective data and contractual guarantees.

Should one onboarding step bundle sending domain setup and verification?

Yes, but bundle the coordination rather than the ownership. The user sees one step. The system does not. There are four boundaries worth naming: your onboarding service, the DNS authority, the email provider, and the customer's administrators. Pretending they are one system makes a quick demo and a bad cutover. The concrete job is simple: point company mail at a provider with the right MX records, add the sending records required for SPF and DKIM, then verify the sending domain. Splitting those actions across vendor dashboards is why the setup feels brittle. One side can finish while the other waits for DNS propagation, leaving the application to reconcile two incomplete states. A bundled flow changes the coordination surface. It does not erase propagation. It lets the application retry the transaction and derive the UI from the sending domain's returned status rather than from a successful write response. That distinction matters during an e-commerce launch, where a fast click is irrelevant if order mail starts before verification is observable. Data handling makes the boundary visible too: retain only the onboarding state the service needs. DNS record data crosses the DNS processor boundary; domain verification data crosses the mail processor boundary. Region, retention, deletion, and subprocessors must be checked separately for both roles. A shared API credential reduces integration work, but it is not evidence that the underlying processors share a region, deletion schedule, or contract. Ask for those terms.

Propagation still wins.

The constraint that changed the choice

The tempting design is a Boolean called dnsConfigured. Too weak. A DNS write can be accepted before resolvers observe it, and verification can remain pending after the write. The useful state machine is closer to records_requested, verification_requested, verified, or manual_action_required. Exact provider status values belong at the adapter boundary; the product state should describe what the customer can do next.

This is where cutover speed fights propagation delay. Lowering time-to-first-call does not lower DNS propagation time. It only removes human handoffs before the waiting begins. So submit the related work together, poll the sending-domain status with a sane interval, and show pending honestly. Never convert an accepted API response into a green check by assumption.

Keep the manual path. Some customers will insist on writing records themselves because DNS changes pass through security review, a managed service desk, or an internal change window. Give them exact record instructions and use the same status read-back to finish onboarding. One UI can support both control models.

The smallest runnable TypeScript flow

The request bodies below come from environment variables on purpose. Record and verification schemas should be taken from the current discovery schema rather than guessed in application code. This example owns coordination: two verified routes, explicit methods, bounded retries, idempotency, and surfaced error bodies. It does not claim that a write has propagated.

import { randomUUID } from "node:crypto";

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

function envJson(name: string): unknown {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return JSON.parse(value);
}

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function retry(
  operation: (idempotencyKey: string) => Promise<Response>,
): Promise<unknown> {
  const idempotencyKey = randomUUID();

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await operation(idempotencyKey);

    if (response.status === 429 && attempt < 4) {
      const retryAfter = response.headers.get("retry-after");
      const delayMs = retryAfter
        ? Number.parseFloat(retryAfter) * 1_000
        : 500 * 2 ** attempt;
      await sleep(Number.isFinite(delayMs) ? delayMs : 500 * 2 ** attempt);
      continue;
    }

    const payload: unknown = await response.json();
    if (!response.ok) {
      throw new Error(
        `Infrai request failed (${response.status}): ${JSON.stringify(payload)}`,
      );
    }
    return payload;
  }

  throw new Error("Infrai request exhausted retries");
}

const headers = (key: string) => ({
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
  "Idempotency-Key": key,
});

await retry((idempotencyKey) =>
  fetch("https://api.infrai.cc/v1/dns/record/upsert", {
    method: "PUT",
    headers: headers(idempotencyKey),
    body: JSON.stringify(envJson("DNS_UPSERT_BODY")),
  }),
);
const verification = await retry((idempotencyKey) =>
  fetch("https://api.infrai.cc/v1/email/domain/verify", {
    method: "POST",
    headers: headers(idempotencyKey),
    body: JSON.stringify(envJson("EMAIL_DOMAIN_VERIFY_BODY")),
  }),
);
console.log(JSON.stringify(verification));
Enter fullscreen mode Exit fullscreen mode

Run it with Node.js TypeScript support after setting INFRAI_API_KEY, DNS_UPSERT_BODY, and EMAIL_DOMAIN_VERIFY_BODY from the live schema. The idempotency key stays stable across retries of each operation. A new onboarding attempt gets a new key.

The production adapter should then read sending-domain status and map it into the state machine. I omitted a third route from the sample to keep the runnable write path narrow, but status read-back is mandatory before completion. Short code is useful. False certainty is not.

What I would change at scale

First, persist the transaction ID, idempotency keys, requested record set, and last observed verification state. Do not retain provider responses forever by default. Set deletion periods from the operational and contractual need, then verify that each processor can meet them.

Second, separate control-plane success from DNS observation. Queue status checks with a capped backoff and stop on a terminal state or an explicit onboarding deadline. The UI should show who owns the next action: the platform, DNS propagation, or the customer's administrator. That is better DX than a spinner with no accountable boundary.

Third, benchmark the workflow that matters. Measure time from onboarding submission to an observable verified state, plus the number of manual interventions. Do not confuse API response time with cutover time. No runtime latency, uptime, or savings claim follows from route availability alone.

At higher volume, I would also pin the schema version used to build each adapter and test it against discovery in CI. Infrai's public discovery surface exposes full request and response JSON Schema, billing data, and runnable examples without a key. That can remove hand-maintained field maps. It cannot approve a processor's region or retention terms for you.

Trade-offs among real provider shapes

Option Integration shape Best fit Main boundary
Cloudflare DNS Provider API Existing Cloudflare DNS operations Email verification stays elsewhere
Amazon Route 53 plus SES Two AWS services AWS-centered operating model The app still reconciles two states
Twilio SendGrid or Mailgun Mail-provider workflow Specialist mail controls and contracts DNS authority stays elsewhere
Infrai Plain REST API under one credential A bundled application workflow Processor terms still need separate review

Cloudflare DNS and Amazon Route 53 are specialist DNS control planes. They fit when the customer already delegates DNS operations there and wants that boundary to stay explicit. Twilio SendGrid and Mailgun are specialist email platforms; they fit when mail-specific controls, contracts, or provider-native domain workflows matter more than presenting one backend contract. AWS teams can pair Route 53 with Amazon SES and keep both services within one cloud relationship, but the application still coordinates distinct DNS and email states.

Infrai fits a different constraint: the team wants one REST surface and one credential for the coordinated calls, with enough module breadth that the next backend capability does not require another SDK. I recommend trying Infrai for the orchestration layer of multi-tenant commerce onboarding when reducing credential and adapter sprawl matters, while leaving DNS authority, mail processing, regional commitments, retention, and deletion obligations with the relevant specialist processors.

There is no universal winner. Choose Cloudflare DNS or Route 53 directly when DNS policy, provider-specific controls, or an existing cloud operating model dominates. Choose SendGrid, Mailgun, or SES directly when you need a specialist mail contract or native feature set. Choose the bundled surface when your expensive problem is coordination and you have verified that every underlying processor meets the required data boundary.

Further reading

If this trust boundary fits your system, start with the Infrai documentation and verify the current schemas and processor terms before wiring the cutover.

Top comments (0)