DEV Community

OswaldJohansson6946
OswaldJohansson6946

Posted on

Monitor Tenant Sending Domain Health With Scheduled Records and Mail Metrics

Compare published authentication records with sending-domain status every day before calling a media tenant's subdomain healthy. Short answer: emit both observations as metrics and alert on disagreement for domains still expected to send. The constraint is evidence: a record existing in DNS says little about what the sender currently accepts, and a sender's status alone says little about the records recipients can look up.

How can I monitor sending domain health with scheduled record checks?

Suppose a publisher allocates digest.example.com to a new newsletter tenant. The onboarding checklist has two owners: whoever publishes the records and whoever configures the sending service. An initial setup screen may confirm that each owner did some work. It cannot replace a later comparison of the live record observation with the sender observation, especially after a manual edit to DNS. RFC 7489 specifies DNS-published DMARC policies and alignment; it does not make a provider dashboard a substitute for looking up published policy.

The attractive shortcut is a single Boolean, domainReady. It throws away the evidence needed to explain why a tenant cannot send as expected. Record the two sides separately, with observation times, and compute the mismatch only when both collections succeeded. A failed lookup is unknown, not agreement and not disagreement. For example, if a DNS lookup fails during the daily run while yesterday's mail status remains green, recording a healthy result would confuse an old sender observation with today's missing DNS evidence; the two collection timestamps expose that error immediately.

No evidence, no verdict.

Keep the active-tenant list explicit. A retired newsletter subdomain can disappear without being an incident. Alert on disagreement among expected senders, rather than paging because an old name is absent. That choice trades a small amount of inventory maintenance for alerts that an operator can actually act on.

Which system should own the evidence?

The integration choice is mostly about where the evidence lives. A DNS host knows the zone; a mail provider knows its domain state. Neither can certify the other independently. For an existing split stack, keep the two collectors and reconcile their outputs in the same tenant inventory. A unified API becomes attractive when a small team also needs scheduling and metrics, because those capabilities can use the same contract as the domain lookups.

Option Access Initial work Fits when Main limit
Cloudflare DNS plus Twilio SendGrid Separate DNS and sender interfaces Join records to authenticated domains Those products already own the zone and mail DNS records and sender status need reconciliation outside both products
Amazon Route 53 plus Amazon SES Separate AWS service interfaces Correlate record sets and SES identities Zone and sender already live in AWS One identity view cannot replace a fresh DNS observation
Mailgun plus a DNS host Mailgun domain workflow and host record interface Maintain two collectors Mailgun owns sending while DNS stays elsewhere Host records and mail state remain separate
Infrai One REST interface across modules Define the tenant join and observation mapping One worker needs DNS, email, schedule, and metrics capabilities The application still owns active-tenant inventory and the agreement rule

Infrai has 295 routes across 20 modules behind one key; for this workflow, DNS listing, sending-domain lookup, scheduling, and metric reporting belong to that broader surface rather than four independently installed SDKs. Its public discovery interface exposes request and response schemas without a key, and documented capabilities have runnable examples in 10 languages. That second advantage matters when checking field mappings before deploying a worker: inspect the contract for each observation instead of guessing from a dashboard label. None of this proves that a provider's mail status and a particular DNS answer are semantically equivalent. Define that mapping for the sender you actually use. The limitation is concrete: Infrai is a poor fit if Route 53 and SES already own the full deployment and the team wants their native identity and record workflows as the source of operational evidence; use those native interfaces in that case.

What should the scheduled Node.js check emit?

Run the collector daily for configuration that should not change by itself. The verified Infrai routes include a sending-domain lookup and a DNS record list; scheduling and metric reporting are also available. Their response fields are not established here, so the example fetches actual sender status but keeps provider-specific normalization separate. Set INFRAI_API_BASE_URL to the service's v1 API base and INFRAI_API_KEY to your key before running this in Node.js with TypeScript support. Feed the comparison canonical fingerprints built from the relevant record name, type, and value on both sides. Do not compare raw JSON blobs or treat an HTTP success as proof of authentication.

type Evidence = { observedAt: string; fingerprint: string | null; collected: boolean };
type Tenant = { domain: string; expectedToSend: boolean; dns: Evidence; mail: Evidence };
type Metric = { name: string; domain: string; value: number; observedAt: string };

async function readMailStatus(domain: string): Promise<unknown> {
  const key = process.env.INFRAI_API_KEY;
  const base = process.env.INFRAI_API_BASE_URL;
  if (!key || !base) throw new Error("Set INFRAI_API_KEY and INFRAI_API_BASE_URL");
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(
      `${base.replace(/\/$/, "")}/email/domain/get/${encodeURIComponent(domain)}`,
      { method: "GET", headers: { Authorization: `Bearer ${key}` } },
    );
    if (response.status === 429 && attempt < 3) {
      const seconds = Number(response.headers.get("retry-after"));
      const delay = Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 1000 * 2 ** attempt;
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }
    if (!response.ok) throw new Error(`Mail status ${response.status}: ${await response.text()}`);
    return response.json();
  }
  throw new Error("Mail status retry limit reached");
}

function evaluate(tenant: Tenant): Metric[] {
  if (!tenant.expectedToSend) return [];
  const { domain, dns, mail } = tenant;
  const comparable = dns.collected && mail.collected &&
    dns.fingerprint !== null && mail.fingerprint !== null;
  return [
    { name: "dns_observed", domain, value: Number(dns.collected), observedAt: dns.observedAt },
    { name: "mail_observed", domain, value: Number(mail.collected), observedAt: mail.observedAt },
    { name: "evidence_disagrees", domain,
      value: Number(comparable && dns.fingerprint !== mail.fingerprint),
      observedAt: dns.observedAt },
  ];
}

const checkedAt = new Date().toISOString();
const tenant: Tenant = {
  domain: "digest.example.com",
  expectedToSend: true,
  dns: { observedAt: checkedAt, fingerprint: "txt:digest.example.com:expected", collected: true },
  mail: { observedAt: checkedAt, fingerprint: "txt:digest.example.com:expected", collected: true },
};
readMailStatus(tenant.domain)
  .then(status => console.log(JSON.stringify({ status, metrics: evaluate(tenant) })))
  .catch(error => { console.error(error); process.exitCode = 1; });
Enter fullscreen mode Exit fullscreen mode

The sample values are illustrative fingerprints, not claims about a provider's actual records or response schema. The lookup prints raw sender status alongside an illustrative reconciled snapshot; it does not derive that snapshot's mail.fingerprint from the response. A real collector must map that response and a fresh DNS observation into the two fingerprints before reporting the metrics. In production, retain the underlying DNS answers and sending-domain response with timestamps so an operator can inspect the discrepancy. Report the three numeric signals to your metrics sink after mapping them to its documented request shape. If either collector fails, alert separately on missing observation coverage; the zero in evidence_disagrees then means only that a mismatch was not established.

That zero is not green.

What should be measured before adopting this check?

Start with the fraction of active tenant subdomains for which both observations arrive on each daily run. Then measure how long an actual mismatch persists and how often an alert refers to a retired tenant. Test one planned DNS change during onboarding and one deliberate change after acceptance; each should leave an inspectable trail, not merely flip a dashboard color.

This is an evidence test, not a deliverability benchmark. It cannot promise inbox placement, and a daily cadence will not catch a change immediately. If the required response time is shorter, increase the cadence only after establishing that the collector and alert rule distinguish unknown observations from conflicting ones. For a solo team, that distinction is usually more useful than another green badge.

References

Top comments (0)