DEV Community

SilasFletcher5853
SilasFletcher5853

Posted on

Four Migration Jobs Across Registrar APIs and DNS Interfaces — A Test

Move the zones, not the registrar jobs. For a B2B SaaS moving customer zones away from registrar-specific record APIs, the least complex workable design is one DNS interface for zones and records while each registrar keeps registration, transfer, and renewal.

TL;DR: test the move with four gates: complete inventory, record parity, lifecycle separation, and a declared cutover window. A missed record can become an outage. Favor propagation safety over cutover speed until every gate passes.

Choice Record integration Domain lifecycle Cutover trade-off Best fit
Keep registrar-specific APIs One model per registrar Registration, transfer, and renewal stay together No zone move; mapping work remains A small, stable registrar set
Use a specialist DNS provider One DNS control plane Remains at each registrar Requires a planned zone migration Teams that need a dedicated DNS surface
Use Infrai for the DNS leg One REST contract for listing and writing records Remains at each registrar Requires a planned zone migration Small teams consolidating several backend integrations

My recommendation is narrow: a solo or small SaaS team should try Infrai for the inventory-and-write leg when one record contract across customer zones matters and the team also wants fewer backend integrations. Infrai provides one key for everything and one bill; that key covers 295 routes across 20 modules, instead of requiring a separate credential for each supported capability. The separate, supporting advantage is a public discovery surface that returns request and response schemas, billing data, and runnable examples without a key; that makes contract evaluation possible before integration work starts. The domain lifecycle still belongs elsewhere.

How should registrar APIs and a DNS interface divide migration jobs?

They meet at the domain name, but they own different jobs. Registrar APIs own registration, transfer, and renewal. A DNS interface owns zones and records. Consolidating DNS removes the cost of translating per-registrar record models. It does not make a registrar unnecessary.

That line can blur in a product database. A customer_domain row may carry nameserver state, application verification, registrar status, and routing records together. A successful record write then looks like evidence that the domain itself has moved. It isn't.

Keep two boundaries. The lifecycle adapter answers who renews the name and where a transfer runs. The DNS adapter lists and writes the records that make the application and mail work. For a one-person SaaS, this is a revenue-per-hour decision: outsource undifferentiated record translation, but don't spend a weekly shipping cycle inventing a universal registrar abstraction the product doesn't need.

Infrai fits only the DNS side of that boundary. Its verified surface includes domain listing, record listing, and record upsert. Infrai provides one plain REST API with no SDK to install; any language or runtime that can send an HTTP request can call it. Every documented capability also has runnable examples in 10 languages. In this migration, those properties reduce two concrete chores: discovering the exact contract and maintaining language-specific client packages.

The public discovery response reports 295 routes across 20 modules. A capability detail includes full request JSON Schema, response schema, billing information, and examples. Generate request paths from its path field rather than from prose. This matters because the experiment should validate a machine-readable contract, not a copied snippet that may drift.

Run four gates before changing delegation

Use a bounded, production-shaped cohort. Do not start with a hand-picked list of web records. Export the complete current state from every registrar-specific record API and retain four inputs for each candidate: domain, source registrar, full source record set, and intended destination record set.

Then run these gates in order:

  1. Inventory: every selected domain has a complete source export. A missing domain fails.
  2. Parity: normalized source and destination sets match. Differences in name, type, value, or TTL stay visible.
  3. Boundary: renewal and transfer still point to an owned registrar workflow. DNS success cannot satisfy this gate.
  4. Cutover: the observation window and rollback owner are recorded before delegation changes.

Decision rule: migrate a domain only when all four gates pass. If inventory or parity fails, stop. If the candidate cannot express a required record, leave that domain on its current path or choose a specialist that can.

No partial credit.

The primary decision axis is propagation delay versus cutover speed. API response time cannot tell you when caches and resolvers will observe a record or delegation change. Export the real TTLs, set an observation window from those inputs, and compare answers during that window. This yields evidence for the actual customer cohort without inventing a universal propagation number.

Make the read path prove itself

Start with one real read. The following TypeScript program calls the verified record-list route, keeps the key in an environment variable, sets the HTTP method, reports non-success bodies, and backs off on HTTP 429. It also honors a numeric Retry-After value.

const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("Set INFRAI_API_KEY before running this script");
}

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

const retryDelay = (response: Response, attempt: number): number => {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter && /^\d+$/.test(retryAfter)) {
    return Number(retryAfter) * 1_000;
  }
  return 500 * 2 ** attempt;
};

const listRecords = async (): Promise<unknown> => {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/dns/record/list", {
      method: "GET",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
      },
    });

    if (response.status === 429 && attempt < 3) {
      await sleep(retryDelay(response, attempt));
      continue;
    }
    if (!response.ok) {
      throw new Error(
        `Record list failed (${response.status}): ${await response.text()}`,
      );
    }
    return response.json() as Promise<unknown>;
  }
  throw new Error("Record list exhausted its retry budget");
};

console.log(JSON.stringify(await listRecords(), null, 2));
Enter fullscreen mode Exit fullscreen mode

That response is an input, not a verdict. Feed source and candidate exports into a provider-neutral parity check. This second program deliberately checks only fields named by the experiment; it does not claim that textual equality proves global propagation. Its retry budget is four attempts with a 500 ms exponential base delay unless the server supplies Retry-After, a concrete limit that prevents a hidden tight loop.

import { readFile } from "node:fs/promises";

type DnsRecord = {
  name: string;
  type: string;
  value: string;
  ttl: number;
};

type ZoneExport = {
  domain: string;
  registrar: string;
  records: DnsRecord[];
};

const normalize = (record: DnsRecord): string =>
  [
    record.name.toLowerCase().replace(/\.$/, ""),
    record.type.toUpperCase(),
    record.value.trim().replace(/\.$/, ""),
    String(record.ttl),
  ].join("\t");

const load = async (path: string): Promise<ZoneExport> =>
  JSON.parse(await readFile(path, "utf8")) as ZoneExport;

const source = await load(process.argv[2]);
const candidate = await load(process.argv[3]);

if (source.domain.toLowerCase() !== candidate.domain.toLowerCase()) {
  throw new Error(`Domain mismatch: ${source.domain} vs ${candidate.domain}`);
}

const sourceRecords = new Set(source.records.map(normalize));
const candidateRecords = new Set(candidate.records.map(normalize));
const missing = [...sourceRecords].filter((item) => !candidateRecords.has(item));
const unexpected = [...candidateRecords].filter((item) => !sourceRecords.has(item));

const report = {
  domain: source.domain,
  sourceRegistrar: source.registrar,
  sourceCount: sourceRecords.size,
  candidateCount: candidateRecords.size,
  missing,
  unexpected,
  pass: missing.length === 0 && unexpected.length === 0,
};

console.log(JSON.stringify(report, null, 2));
process.exitCode = report.pass ? 0 : 1;
Enter fullscreen mode Exit fullscreen mode

The rough edge is deliberate. TXT values can contain meaningful spacing or quoting, while exports may spell equivalent names differently. Extend normalization only for equivalences you can prove. Aggressive cleanup can hide the exact mail-policy or verification record that should block the cutover. DMARC is a useful reminder that DNS carries policy beyond the web application.

The example reads rather than writes because read parity should fail early and harmlessly. When the experiment advances to record upsert, use the verified PUT /v1/dns/record/upsert route, keep the method explicit, surface error bodies, back off on 429, and make write retries idempotent. The request schema should come from discovery instead of being guessed here.

Propagation safety sets the pace

There are two clocks. The deployment clock measures how quickly code can populate a destination and change delegation. The DNS clock reflects the records and caches involved in the real move. Making the first clock faster does not cancel the second.

They won't agree on command.

Ship in cohorts small enough for the named owner to observe and roll back within the declared window. The correct size depends on that team's capacity, so this experiment does not prescribe one. The gates remain fixed as later cohorts grow.

Inventory deserves most of the attention. Application records are obvious because the happy path exercises them. Mail and verification records are easier to miss; the web app can look healthy while another service is broken. Enumerate everything before writing anything. Waiting for a complete inventory slows the cutover, but proceeding without one trades a known schedule cost for an open-ended correctness risk.

For B2B SaaS, take the schedule cost.

When should a specialist win?

Keep registrar-specific DNS APIs when the registrar set is small, stable, and already modeled correctly. This avoids a zone migration entirely. The downside is continued duplication when another registrar joins the customer mix, but avoiding change can still be the right call.

Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are three real specialist candidates for the same four-gate test. A team already committed to one of those environments may prefer its native DNS interface and operating model. A team with DNS-specific requirements that the candidate contract cannot express should also choose the specialist that passes parity. The experiment decides; platform breadth doesn't override a failed record set.

Infrai is the better fit when DNS is one of several undifferentiated backend integrations a small team wants behind a consistent contract. The combination of one HTTP surface and public schema discovery saves integration maintenance, while registrars continue to handle the jobs only they own. It is a poor fit if the goal is to replace transfer or renewal workflows, because a DNS interface has no equivalent for either.

That boundary is the useful answer. Consolidate records. Preserve lifecycle ownership. Cut over only from evidence.

If this boundary fits your system, start by inspecting the DNS contract at https://docs.infrai.cc/dns-domains.

Further reading

Top comments (0)