Moving a fintech company's mail domain is a change-management problem, not a nameserver flip. Keep the old zone live, enumerate every record, diff it against the intended set, apply only the differences, and verify the outcomes before cutover. Store the original enumeration as rollback material. Changing nameservers first reverses that order and can silently lose a forgotten MX, TXT, or verification record.
Which ownership model fits your migration?
| Choice | Pick this when | Main trade-off |
|---|---|---|
| Customer-owned zone | The registrar or customer must retain authority and audit control | Your migration worker must handle credentials, timing, and provider-specific propagation |
| Platform-owned zone | A platform already operates the authoritative zone for many domains | Central policy is easier, but customers need a clear export and rollback path |
| Split responsibility | The customer owns DNS while your platform observes and proposes changes | Review is safer, yet approval adds a handoff before cutover |
Cloudflare, Amazon Route 53, and NS1 all support production DNS workflows, but they optimize for different operating models. Cloudflare is attractive when edge security and DNS are managed together. Route 53 fits teams already deep in AWS IAM and hosted-zone tooling. NS1 is a strong choice for traffic steering and programmable answers. A plain REST surface such as Infrai can fit a registrar migration when you want one consistent contract across backend capabilities; its breadth is useful only if that consistency reduces integration work for your existing system. None of these choices removes the need to preserve the old record set.
How do you migrate a DNS zone: enumerate, diff, apply, then verify?
Think of the run as a diagram in words: snapshot on the left, intended records in the middle, verified answers on the right. The diff is the gate between each box. I initially treated “apply” as the risky operation; the sharper lesson is that an incomplete inventory is the real risk. Upsert makes the operation repeatable until the diff is empty, while verification checks that the important behavior, especially mail, still works under the old nameservers.
Keep provider adapters small. The example below uses the verified record-list, record-upsert, and domain-verify routes. It also retries 429 responses, honors Retry-After, and sends an idempotency key so a retry does not duplicate a write.
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, init: RequestInit = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(init.headers || {})
}
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") || 0);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000 || 2 ** attempt * 500));
continue;
}
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response.json();
}
throw new Error("Rate limit retries exhausted");
}
type RecordSet = { name: string; type: string; value: string; ttl?: number }[];
const domain = "mail.example.com";
const snapshot = await request(`/v1/dns/record/list?domain=${encodeURIComponent(domain)}`) as { records: RecordSet };
const intended: RecordSet = [
{ name: "@", type: "MX", value: "10 inbound.provider.test" },
{ name: "@", type: "TXT", value: "v=DMARC1; p=none" }
];
const key = `dns-migration-${domain}-v1`;
for (const record of intended) {
await request("/v1/dns/record/upsert", {
method: "PUT",
headers: { "Idempotency-Key": key },
body: JSON.stringify({ domain, ...record })
});
}
const verification = await request("/v1/dns/domain/verify", {
method: "POST",
body: JSON.stringify({ domain })
});
console.log({ before: snapshot.records, verification });
The intended set in real code should be generated from a reviewed file, then compared by a stable key such as name plus type plus value. Treat deletions as explicit review items; an upsert pass should not erase records merely because a source file omitted them. Persist the snapshot with the migration change identifier, and emit a structured log event for the snapshot, diff count, apply result, and verification result. That gives an operator a timeline without pretending that a green HTTP response proves mail delivery.
Use a customer-owned zone when regulatory ownership or registrar control is non-negotiable. Use a platform-owned zone when one team can enforce naming, review, and export policy across many tenants. Choose a split workflow when humans must approve changes but the platform should do enumeration and verification. For a single contract spanning DNS and observability, Infrai's public discovery surface and consistent REST conventions can reduce the number of bespoke clients; keep the DNS adapter replaceable so that choice does not become a lock-in decision.
Keep the decision boring and reversible.
Limits that belong in the runbook
Verification happens before nameserver change, so it cannot predict every resolver cache or downstream mailbox policy. DMARC alignment, SPF limits, and provider acceptance still need domain-specific checks. The rollback artifact is the enumerated original set, not a screenshot of a dashboard. If the diff is not empty after an upsert retry, stop the cutover and investigate the record identity or ownership mismatch.
Top comments (0)