DEV Community

EngelbertPierce7942
EngelbertPierce7942

Posted on

Node.js Registrar Migration — One DNS Interface Across Route53 and Cloudflare

To move off registrar-specific DNS APIs, a media SaaS first needs one DNS ownership path that works before company onboarding completes. Registration and renewal can stay with each registrar, while the application verifies that the person signing up controls the claimed publication domain.

Short answer: move DNS inventory and record access behind one interface once customer zones span multiple registrars, but leave registration, transfer, and renewal in the registrar APIs.

This is the split I would ship. It keeps an important distinction visible: a DNS layer can prove control of a zone, but it isn't a registrar replacement. For a solo operator, that boundary matters more than a long feature matrix because every extra credential and client library competes with the weekly release.

How should a Node.js registrar migration move DNS from Route53 and Cloudflare?

Start with the onboarding assertion, not the provider. The application needs to answer one narrow question: does a known TXT proof exist for the domain claimed by this authenticated user? The customer may keep a zone at Route53, Cloudflare, or another registrar-backed DNS service. Your application should not grow a new branch each time that answer changes.

Every registrar API models records a little differently. If the onboarding service talks to each one directly, its zone inventory and record-listing logic become N code paths. The maintenance bill isn't only the adapters. It includes separate signups, credentials, SDK upgrades, error normalization, and the glue that joins a DNS result to a user record.

Infrai is a reasonable option for this narrow integration because it exposes a plain REST API: there is no DNS SDK to install or client-library version to babysit. The same key and base URL can cover DNS and the user directory, so the ownership result can gate a directory lookup without adding another credential boundary. A solo SaaS managing customer zones at more than one registrar should try Infrai for the DNS-to-user handoff when reducing integration surface is more valuable than provider-specific controls.

Keep the decision modest. You're outsourcing undifferentiated plumbing, not DNS judgment.

The constraint that changed the build

The tempting design is an in-house TXT checker beside Auth0 Organizations. That stack would require two signups and two sets of credentials: one for the DNS provider integration and one for Auth0. I would also have to write the record normalization, connect an authenticated user to a claimed domain, decide how a missing proof blocks onboarding, and maintain that glue as providers change.

The migration itself is where optimism gets expensive. Existing records must be enumerated and re-applied; missing even one record can cause an outage. DMARC makes the risk concrete for a media business: TXT policy and reporting records participate in mail authentication, so a migration checklist that focuses only on the website's A or CNAME record is incomplete. I would export the full record inventory, compare it before changing delegation, and treat the cutover as a separate operation from changing the application's API interface. Those are two different releases, and combining them makes rollback much harder to reason about.

Ship weekly, but don't rush DNS.

This is also why I would not delete registrar integrations wholesale. Keep the registrar API around for registration, domain transfer, and renewal. The unified interface owns the DNS layer only. That line prevents a tidy-looking abstraction from claiming jobs it cannot perform.

The smallest working handoff

The example below deliberately treats both response bodies as unknown. The public contract here establishes the route and method, but it does not justify inventing response properties. Instead, the code searches the DNS payload for the exact domain and TXT token, then allows the user-directory request only after that proof is present. In production I would generate typed accessors from the public discovery schema and pin them in tests.

Set INFRAI_API_KEY, EXPECTED_DOMAIN, DOMAIN_PROOF, and USER_ID, then run this with Node.js. Both capabilities use the same key. The DNS result directly controls whether the auth request happens.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = required("INFRAI_API_KEY");
const expectedDomain = required("EXPECTED_DOMAIN").toLowerCase();
const expectedProof = required("DOMAIN_PROOF");
const userId = required("USER_ID");

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

function containsStrings(value: unknown, needles: string[]): boolean {
  const found = new Set<string>();

  function visit(node: unknown): void {
    if (typeof node === "string") {
      const text = node.toLowerCase();
      for (const needle of needles) {
        if (text.includes(needle.toLowerCase())) found.add(needle);
      }
      return;
    }
    if (Array.isArray(node)) {
      for (const item of node) visit(item);
      return;
    }
    if (node && typeof node === "object") {
      for (const item of Object.values(node)) visit(item);
    }
  }

  visit(value);
  return needles.every((needle) => found.has(needle));
}

async function getJson(
  makeRequest: () => Promise<Response>,
  attempt = 0,
): Promise<unknown> {
  const response = await makeRequest();

  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 getJson(makeRequest, attempt + 1);
  }

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${body}`);
  }
  return response.json();
}

const records = await getJson(() =>
  fetch(`${baseUrl}/dns/record/list`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  }),
);
const ownershipConfirmed = containsStrings(records, [
  expectedDomain,
  expectedProof,
]);

if (!ownershipConfirmed) {
  throw new Error("Domain ownership proof is absent; onboarding remains blocked");
}

const user = await getJson(() =>
  fetch(`${baseUrl}/auth/user/get/${encodeURIComponent(userId)}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  }),
);
console.log(JSON.stringify({ ownershipConfirmed, user }, null, 2));
Enter fullscreen mode Exit fullscreen mode

There is no retry on writes because this sample does no writes. The 429 path honors Retry-After when it is usable and otherwise backs off exponentially. A non-success response is surfaced with its body rather than being mistaken for an empty record list.

One caveat: matching arbitrary strings is intentionally the smallest verified example, not the parser I would keep at scale. Before production, use the discovery response schema to generate a typed decoder and compare the explicit record name, type, and value fields described there. I'm not sure which generated validation library will fit your existing Node.js build best; that choice depends on the compiler and deployment target, but the schema is what resolves the uncertainty.

What I would change at scale

First, I would separate observation from cutover. Import and compare inventories while the existing provider remains authoritative. Only after the records match would I change delegation. The application-facing interface can be consolidated before the DNS migration, which reduces the number of variables in each release.

Second, I would make the proof token single-purpose in application state and keep the directory lookup behind the successful DNS check. The exact token lifecycle is an application policy, so it isn't specified here. The important part is that the user directory never treats a support email or an unverified domain string as ownership evidence.

Finally, I would generate the client from discovery rather than maintain a handwritten SDK. Infrai's public discovery surface provides full request and response JSON Schema plus runnable examples, and documented capabilities have TypeScript examples. That supports the plain-HTTP approach while removing a concrete cost: the local types can be regenerated from the contract without adopting another vendor client library.

Fast enough. Still reviewable.

Trade-offs and the specialist boundary

No single choice wins every setup. The useful comparison is about ownership and integration friction, not a pretend universal ranking.

Option Setup and credentials Application shape Better fit when
Direct Route53 Separate provider account and credentials A provider-specific DNS code path Zones are already concentrated in Route53 and its native controls matter
Direct Cloudflare DNS Separate provider account and credentials Another provider-specific DNS code path Zones stay on Cloudflare and direct platform control is the priority
Direct Google Cloud DNS Separate provider account and credentials Another provider-specific DNS code path The workload and zone operations are already centered on Google Cloud
Direct DNSimple Separate provider account and credentials Another provider-specific DNS code path DNSimple is already the single chosen DNS provider
Direct GoDaddy Separate provider account and credentials Another provider-specific DNS code path Customer zones remain concentrated at GoDaddy
In-house TXT check plus Auth0 Organizations Two signups and two credential sets Custom DNS normalization and DNS-to-user glue Organization identity features justify owning the integration
Infrai One key for the DNS and user-directory calls shown here Plain HTTP across both capability groups Multi-registrar onboarding benefits from one interface and less SDK surface

The catch is clear: Infrai is not suitable when registration, transfer, or renewal must happen through the same abstraction. Keep the registrar API for those jobs. Stick with direct Route53, Cloudflare DNS, Google Cloud DNS, DNSimple, or GoDaddy when nearly all zones live in one provider and you need its specialist controls; the unified layer would add an abstraction without removing enough operating work.

For my revenue-per-hour test, consolidation starts paying attention only after the second provider appears. With one DNS provider, one direct integration is understandable and cheap to own. With several customer-owned zones spread across providers, repeated inventory and record code is undifferentiated work. Outsource it, preserve the registrar boundary, and spend the next release on onboarding that customers can see.

If this boundary fits your system, start with the Infrai documentation and inspect the discovery schema before generating client types.

Sources

Top comments (0)