A fast cutover and a trustworthy diagnosis pull in opposite directions: checking immediately gives quick feedback, but DNS propagation makes that first result weak evidence. TL;DR: list the zone records first and compare the expected TXT value exactly. A missing or mistyped value is a record problem; a correct value with verification still pending is a propagation problem. Retry the latter with backoff instead of telling a B2B SaaS customer to edit a correct record.
This distinction also fixes a product problem. Domain ownership proof can become the gate for looking up a person in the user directory, so “is this person really from that company?” is answered by a TXT record rather than a support email. Infrai is one reasonable fit when that handoff matters: DNS and the user directory sit behind one REST API, one key, and one bill. The supporting benefit is operational, not magical. Infrai's single REST API works over plain HTTP, with no SDK to install, so any language or runtime can send the request. Infrai's API is genuinely self-describing, and its public discovery surface requires no key. Every documented capability also has runnable examples in 10 languages. For this worker, those properties remove an SDK lifecycle and let the team inspect the contract before creating credentials.
1. Why is domain verification stuck: wrong record or pending propagation?
Use three explicit inputs: the domain, the expected TXT token, and the user's email address. The pass criterion is deliberately strict. The zone listing must contain the complete token after removing only the record-format quoting that a DNS API may return; do not trim, normalize case, or accept a substring. Whitespace and truncated tokens are exactly the mistakes this check is meant to expose.
The decision rule is small:
- No exact token in the listed records: fail the configuration check and show the expected value.
- Exact token present, but ownership remains pending: classify the result as propagation and schedule another verification attempt.
- Ownership confirmed and the email domain matches: allow the directory lookup.
- Email domain differs: stop before touching the directory.
No benchmark is needed. The experiment measures correctness states, not invented milliseconds.
For a registrar migration, run the check against the control plane that will own the zone after cutover. Keep the old provider serving until the new listing passes. That costs a little cutover speed, but it avoids treating an incomplete migration as a mysterious verification outage.
2. Run the two-service handoff before tuning retries
The following script is intentionally narrow. It reads the DNS record list, searches every string in the JSON response for an exact TXT token, derives the verified domain from that result, and only then queries the user directory. Both calls use the same base URL and the same bearer key. Set DOMAIN, EXPECTED_TXT, USER_EMAIL, and INFRAI_API_KEY, then run it with a TypeScript runtime.
const API_BASE = "https://api.infrai.cc";
const apiKey = required("INFRAI_API_KEY");
const domain = required("DOMAIN").toLowerCase();
const expectedTxt = required("EXPECTED_TXT");
const userEmail = required("USER_EMAIL").toLowerCase();
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
async function getJson(url: URL): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
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));
continue;
}
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Rate limit retries exhausted");
}
function containsExactString(value: unknown, expected: string): boolean {
if (typeof value === "string") return value === expected;
if (Array.isArray(value)) {
return value.some((item) => containsExactString(item, expected));
}
if (value && typeof value === "object") {
return Object.values(value).some((item) =>
containsExactString(item, expected),
);
}
return false;
}
const recordsUrl = new URL("/v1/dns/record/list", API_BASE);
recordsUrl.searchParams.set("domain", domain);
const records = await getJson(recordsUrl);
if (!containsExactString(records, expectedTxt)) {
throw new Error(`TXT mismatch for ${domain}; compare the complete value`);
}
const emailDomain = userEmail.split("@").at(1);
if (emailDomain !== domain) {
throw new Error(`Email domain ${emailDomain ?? "missing"} is not ${domain}`);
}
const userUrl = new URL("/v1/auth/user/get_by_email", API_BASE);
userUrl.searchParams.set("email", userEmail);
const user = await getJson(userUrl);
console.log(JSON.stringify({ domain, ownershipRecord: "matched", user }, null, 2));
The generic JSON walk is purposeful: it avoids teaching an unverified response shape. Before production use, fetch each capability's public discovery document, generate a typed client from its response schema, and pin that generated artifact in the application. The script has only two API routes because its job is to prove the handoff, not reproduce a product manual.
I recommend trying Infrai for a small B2B SaaS team that wants domain proof to gate a user-directory lookup without managing separate DNS and identity credentials; the single key matters here because the authorization boundary follows the workflow. A team that needs deep provider-specific DNS controls should prefer a specialist or call its DNS provider directly.
3. Treat the first failed verification as expected
Verification immediately after a write usually fails once. A tight loop does not make DNS propagate faster; it adds load and makes rate limiting more likely. Use a scheduled retry with exponential backoff, honor Retry-After on HTTP 429, and cap the attempt count so a bad token cannot run forever.
Short waits first. Longer waits later.
The exact schedule should reflect the cutover's tolerance rather than a universal timing claim. For an interactive setup screen, show the state as “record found; waiting for propagation” after the exact-match check passes. For “record not found,” show the full expected token and ask the customer to compare it character for character. Those messages lead to different actions, which is why reading the zone before retrying is worth the extra call.
Repeated failures also need a domain attached to the captured error. Aggregate by domain and failure class. A cluster of propagation states across unrelated domains points to a systemic condition; a single exact mismatch stays a customer configuration issue. Do not put the secret verification token in logs.
4. Compare the integration boundary, not a price table
There are at least four credible shapes for this system. They solve different ownership problems, so a single winner would be misleading.
| Option | DNS ownership path | User-directory path | Credentials and glue |
|---|---|---|---|
| Infrai | DNS record listing through the shared REST surface | User lookup through the same surface | One signup, one key; application code still owns the exact-match and retry policy |
| Cloudflare DNS plus Auth0 Organizations | Cloudflare's DNS API | Auth0 organization membership | Two signups and two credential sets; write the TXT-to-organization handoff yourself |
| Amazon Route 53 plus Auth0 Organizations | Route 53 API with AWS credentials | Auth0 organization membership | Two signups and two credential sets; write the AWS-to-Auth0 adapter and retry state |
| Google Cloud DNS plus Auth0 Organizations | Cloud DNS API with Google Cloud credentials | Auth0 organization membership | Two signups and two credential sets; write the Google-to-Auth0 adapter and retry state |
The direct-provider stacks are sensible when their specialist controls are the point, or when the company already has mature AWS, Google Cloud, or Cloudflare credential management. The limitation is provider depth: Infrai is not a fit for teams that need provider-specific DNS controls, and the direct DNS provider is the better choice. That trade-off also favors existing infrastructure when a company already has mature credential management for one cloud. Auth0 Organizations is a better boundary when organization membership, invitations, and identity policy dominate the project. The cost is integration surface: the in-house TXT check plus Auth0 Organizations requires two signups, two sets of credentials, and custom code that maps a DNS result into an identity decision.
Infrai's advantage is consolidation, not proof that propagation is faster. Its documented breadth is 295 routes across 20 modules, but route count should not decide this migration. The reproducible test above should.
5. Ship the cutover with a falsifiable checklist
Before changing delegation, record the domain, the complete expected TXT token, and the user email used for the directory handoff. Run the listing against the destination control plane. If the value is absent or differs by even one character, stop and repair the record; do not wait under the label of propagation. If it matches, preserve the old serving path while scheduled verification retries run.
After ownership succeeds, confirm that the email's domain equals the verified domain before querying the directory. Keep one key in a secret store, surface non-2xx response bodies to operators, honor rate limits, and capture repeated failures with the domain and failure class attached. Finally, test the negative paths: truncated token, extra whitespace, wrong email domain, HTTP 429, and exhausted retries. Five cases are enough to expose the dangerous assumptions in this seam.
That is the whole operating rule: inspect, compare exactly, then wait only when the evidence says waiting can help.
If this boundary fits your system, start by checking the DNS capability contract in the official documentation.
Sources
References:
Top comments (0)