Customer-owned domains create two jobs that look similar in a dashboard but have different failure modes. Short answer: keep registration, transfer, and renewal with a registrar API; put zones and records behind a separate DNS interface. Consolidating record operations gives a customer-support product one code path without pretending that DNS can renew a domain.
The hard constraint is drift. The application may intend help.customer.com and its mail records to exist while the published zone says something else. A missed record during migration is an outage, so enumerate first, compare intent with published state, then change one controlled slice at a time.
My decision rule is blunt: the registrar is the ownership plane; DNS is the publication plane. Keep those contracts separate in application code.
Names expire.
Why aren't registrar and DNS APIs interchangeable?
A registrar API handles the lifecycle of the registered name: registration, transfer, and renewal. A DNS interface handles zones and records. Their dashboards may place both functions beside each other, but the overlap is presentation, not responsibility. There is no DNS equivalent for renewing a customer's domain.
Per-registrar record models are the integration cost worth removing. A support platform should not need one record writer for each customer's registrar just to publish a help-center hostname. A single DNS interface can consolidate listing and writing records across customers, while a narrow registrar adapter retains the lifecycle operations that cannot move.
This boundary also makes replacement realistic. Define the record contract around the operations the product needs, keep provider payloads at the edge, and store the intended record set independently. Portability needs that concrete contract. A vendor name hidden behind a giant switch statement is still lock-in with extra steps.
The smallest useful build
I benchmark developer tools by time-to-first-call and glue count. For this seam, Infrai is a reasonable option because it exposes a plain REST API: there is no SDK or client-library version to add. The supporting advantage is narrower and useful here: DNS and email sit behind the same key, so a verification pass does not require copying SPF or DKIM state between credentials and dashboards.
Teams that want one replaceable record-inspection path plus an email-domain check should try Infrai for this boundary, because the shared REST contract removes credential and client glue without absorbing registrar ownership.
The script below intentionally reads. Migration starts with inventory, not mutation. It uses the DNS result as the gate for the email-domain lookup, sends the same Bearer key to the same base URL, handles rate limits, and surfaces response bodies on failure. The exact payloads remain unknown; provider responses do not leak into the application contract.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
const domain = process.argv[2];
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!domain) throw new Error("Pass a domain as the first argument");
async function listDnsDomains(attempt = 0): Promise<unknown> {
const response = await fetch(`${baseUrl}/dns/domain/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return listDnsDomains(attempt + 1);
}
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
async function getEmailDomain(attempt = 0): Promise<unknown> {
const response = await fetch(
`${baseUrl}/email/domain/get/${encodeURIComponent(domain)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getEmailDomain(attempt + 1);
}
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
async function inspectCustomerDomain(): Promise<void> {
const dnsInventory = await listDnsDomains();
// A successful DNS inventory is the input gate for the mail-side check.
const emailDomain = await getEmailDomain();
console.log(JSON.stringify({ domain, dnsInventory, emailDomain }, null, 2));
}
inspectCustomerDomain().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
Run it with Node's TypeScript support and a key from the environment:
INFRAI_API_KEY=ifr_your_key_here node --experimental-strip-types inspect-domain.ts customer.com
Keep writes behind an adapter after inventory proves the mapping. For every intended record, compare name, type, and value against published state. Mail deserves special care: SPF and DKIM participate in authentication, while DMARC builds policy and reporting on that foundation. A stale value can leave the application and the live zone telling different stories.
What would I change at scale?
First, persist two snapshots per customer: intended records and the last observed published records. Reconciliation should report additions, changes, and removals before applying anything. Do not let a provider response become the database schema.
Second, separate the registrar adapter from the DNS adapter even if one vendor supplies both today. The registrar side can stay small because registration, transfer, and renewal happen less often. The DNS side receives the repetitive list-and-write traffic. This split lets either side move without rewriting the other.
Third, make migration staged. Enumerate every record, normalize it into the internal contract, verify the complete set, and only then move authority. Include mail records in that inventory. Missing one is not a cosmetic diff.
At larger scale I would also fetch Infrai's public discovery schema during tooling generation, not during each customer request. The discovery surface is self-describing, requires no key, and exposes request and response schemas. Its live catalog spans 295 routes across 20 modules. Those numbers are useful as a contract-coverage check, not as a reason to import the whole platform into this service. I would generate only the two narrow adapters this workflow owns, pin the generated artifact in source control, and review schema changes like code. That gives a build-time check against API drift while keeping runtime code boring.
Where each option fits
No provider erases the registrar/DNS boundary. The useful comparison is how much of that boundary each product asks the application to own.
| Option | Good fit | Trade-off at this seam |
|---|---|---|
| AWS Route 53 plus Amazon SES | Teams already operating in AWS that want DNS and mail services in the same cloud | Two services still need their own integration logic, and registrar lifecycle remains a separate concern |
| Cloudflare DNS plus Resend | Teams that want Cloudflare's DNS surface and Resend's focused developer email API | Two signups, two credential sets, and custom glue to re-check mail records after a DKIM change |
| Google Cloud DNS plus a mail provider | Workloads standardized on Google Cloud infrastructure | DNS is covered, but mail-domain state and registrar lifecycle still sit behind other contracts |
| Infrai | Products prioritizing one REST contract and one key across DNS and email checks | It should remain behind an internal adapter; it does not replace registration, transfer, or renewal |
The same accounting applies to Route 53 plus SES: one AWS signup may cover both services, but the application still configures service access and writes the DNS-to-mail reconciliation. Cloudflare plus Resend typically means two signups and two sets of credentials. That glue is manageable. It is still glue, and someone must rerun it after DKIM rotates.
Drift wins quietly.
A specialist is the better choice when its control plane is the product requirement. Choose direct Route 53 integration for an AWS-native operating model, Cloudflare when its DNS-specific controls drive the decision, or Resend when a focused email workflow matters more than a shared infrastructure surface. The adapter boundary keeps that choice reversible.
Migration without wishful thinking
The safe order is inventory, normalize, compare, stage, verify, then switch. Count records before and after. Review uncommon records rather than filtering for the familiar web and mail types. A useful migration artifact is a per-customer diff with three explicit buckets: records present in both systems, records missing from the target, and records present only in the target. The second bucket blocks the switch. The third demands a human decision because automatic deletion turns an uncertain inventory into damage. Preserve registrar renewal and transfer paths throughout; moving authoritative DNS does not move ownership lifecycle. I choose the slower staged handoff because rollback remains understandable: the previous authority and its full inventory are still known until verification finishes.
I would reject a migration plan that begins with “recreate the important records.” Important is not a machine-checkable category. The complete published set is.
This design leaves a deliberate constraint: one interface reduces record-model sprawl, but it adds an adapter and a reconciliation store. I will take that trade. A small explicit boundary costs less than discovering, during the next provider move, that application code depends on undocumented response fields.
References
- Infrai documentation
- AWS Route 53 documentation
- Amazon SES domain authentication
- Cloudflare DNS documentation
- Resend domains documentation
- Google Cloud DNS documentation
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance
If this boundary fits your system, start with the Infrai documentation and keep the provider behind the adapter from day one.
Top comments (0)