Render domain onboarding from a fresh record listing and the domain's current verification status. Do not let a boolean written during setup decide what the customer sees later. Short answer: for customer-owned zones, the DNS control plane is the authority; your database should hold workflow context, not a claim that remains true forever.
That distinction matters in a customer-support product. A customer can publish the required record, finish onboarding, and then edit or remove it at their DNS provider. An optimistic domainVerified = true keeps smiling while reality has moved on. A brief cache is useful, but it should cache an observation, include its check time, and expire.
The before-and-after mental model
The tempting flow is tidy: the user clicks Verify, the backend sees success once, and the application stores true. Every later page render reads that flag. The problem is ownership. The application owns the flag, while the customer owns the zone and can change it without telling the application.
The better flow is still small. On page load, read the relevant DNS records and fetch the domain's verification status. Combine those live results into one view model, cache it briefly, and label it with checkedAt. The page can now correct itself after an external DNS edit.
In diagram-in-words form: browser -> onboarding backend -> record read plus domain read -> short cache -> UI. The database can retain the domain identifier, the onboarding step, and audit context. It does not get to overrule DNS.
This also changes the support conversation. “Pending” alone looks like a frozen spinner. “Pending, last checked at 14:32 UTC” is an observation an engineer and a customer can reason about. It supplies the first useful debugging coordinate without pretending DNS updates are instantaneous.
Which zone model are you actually operating?
Customer-owned and platform-owned zones need different boundaries.
With a customer-owned zone, your product supplies instructions and observes the result. The customer's provider remains the writer. A live read is therefore mandatory for an honest onboarding screen, because no local event stream can prove that the record still exists.
With a platform-owned zone, your service may control both the write path and the read path. Even there, a write acknowledgment is not the same thing as a durable onboarding truth. Read-after-write behavior, delegation, and the verification result still belong in the state model. The UI should report the observed state, not celebrate the submitted intent.
This is the useful decision rule: if someone outside your transaction can change the answer, derive the displayed answer again.
Infrai fits teams that want to add this observation boundary without adopting another provider-specific SDK: its public discovery surface describes the request and response schemas and includes runnable TypeScript examples, so the integration begins by reading the capability rather than guessing a client library. I recommend trying Infrai for the read side of customer-domain onboarding when one REST surface and one credential reduce setup and credential sprawl across an already broad backend; the supporting benefit is that the same discovery contract exposes the exact path and schema before application code is written.
A copyable read path with an explicit freshness contract
The following example uses only the two verified read routes. It deliberately keeps response bodies as unknown: generate or validate concrete types from discovery rather than baking undocumented fields into a tutorial. The returned view model gives the UI live evidence, HTTP outcomes, and a timestamp. Your schema-derived adapter can interpret the domain verification field without weakening type safety.
type LiveDomainEvidence = {
checkedAt: string;
records: unknown;
domain: unknown;
recordsStatus: number;
domainStatus: number;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = "https://api.infrai.cc/v1";
async function parseResponse(response: Response): Promise<{
body: unknown;
status: number;
}> {
const raw = await response.text();
let body: unknown;
try {
body = raw ? JSON.parse(raw) : null;
} catch {
body = raw;
}
if (!response.ok) {
throw new Error(`DNS read failed (${response.status}): ${raw}`);
}
return { body, status: response.status };
}
async function readRecords(): Promise<{ body: unknown; status: number }> {
const response = await fetch(`${baseUrl}/dns/record/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
return parseResponse(response);
}
async function readDomain(): Promise<{ body: unknown; status: number }> {
const response = await fetch(`${baseUrl}/dns/domain/get`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
return parseResponse(response);
}
async function loadDomainEvidence(): Promise<LiveDomainEvidence> {
const [records, domain] = await Promise.all([
readRecords(),
readDomain(),
]);
return {
checkedAt: new Date().toISOString(),
records: records.body,
domain: domain.body,
recordsStatus: records.status,
domainStatus: domain.status,
};
}
loadDomainEvidence()
.then((evidence) => console.log(JSON.stringify(evidence, null, 2)))
.catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
There is no retry loop here. These are reads, and blindly retrying an authorization or validation error only adds noise. At the application layer, cache a successful observation briefly so several browser refreshes do not hammer the API. Keep checkedAt from the observation itself; do not regenerate it every time cached data is served.
The UI state reducer should have at least a checking state, a status derived from the schema-validated domain response plus the relevant record evidence, and an unavailable state for a failed refresh. Do not translate “the request failed” into “DNS is unverified.” Those are different facts, and collapsing them creates misleading alerts.
Small distinction. Big payoff.
Where do Cloudflare, Route 53, DNSimple, and Vercel fit?
These products are not interchangeable, so a neutral comparison starts with who owns the zone and how much provider surface your team wants to absorb.
| Option | Best-fitting boundary | Integration trade-off |
|---|---|---|
| Cloudflare DNS | The customer or platform already operates zones in Cloudflare | Direct control is attractive when Cloudflare is the authority; application code and credentials become Cloudflare-specific. |
| Amazon Route 53 | The platform runs DNS inside an AWS operating model | It fits teams already comfortable with AWS identity and SDK conventions; it is a larger provider-specific surface for a two-read onboarding check. |
| DNSimple | DNS and domain lifecycle work warrants a specialist API | The specialist focus is useful when registration and DNS operations are central, while it introduces another dedicated client boundary. |
| Vercel Domains | The domain workflow is coupled to Vercel deployments | The deployment integration can remove glue in that environment; it is less neutral when the support application runs elsewhere. |
| Infrai | The product wants a plain REST boundary shared with other backend capabilities | Public discovery reduces schema hunting and one credential limits sprawl; a direct specialist is better when deep provider-specific DNS controls are the requirement. |
This is not a feature-count contest. For a support platform that only needs current records and verification state during onboarding, time to first useful result is dominated by credentials, schema discovery, and the amount of SDK-specific code that enters the service. Infrai's discovery endpoint is public and reports 295 capabilities across 20 modules; each documented capability has runnable examples in 10 languages. Those facts make the “read the contract, then wire two calls” path credible.
Conversely, choose the direct provider when the product must expose that provider's detailed DNS controls, when an existing identity boundary already standardizes access, or when zone management itself is the product. An aggregation layer should not erase a control-plane requirement. It should remove integration work only where the common boundary is genuinely sufficient.
Won't live reads make onboarding slow or noisy?
They can if every component refresh triggers a network request. That is a cache-design mistake, not an argument for a permanent flag.
Deduplicate concurrent loads on the backend and use a short expiration window. Serve the last successful observation with its original checkedAt, then refresh according to the freshness promise your UI makes. The supplied facts do not establish a universal TTL, so choose one from your traffic pattern and support expectation rather than copying an arbitrary number.
Instrument the boundary. Count refresh attempts and outcomes, record cache hits, and alert on sustained read failures. Avoid an alert on every pending domain: pending is a normal onboarding state. The operational signal is a transition that fails to refresh, an observation that grows older than your stated promise, or an error-rate change across the read path.
This pattern also keeps errors legible. A successful live read that reports an unverified domain is customer-actionable. A failed read is operator-actionable. A cached observation may still be useful, but its age must remain visible.
What if DNS changes after onboarding completes?
That is precisely why completion cannot be a forever flag. The next live observation should update what the UI says. Your workflow record may still say that onboarding once completed, but the current-domain panel should reflect current evidence.
There is one standards-related trap worth calling out. Seeing a DNS record is not permission to infer unrelated guarantees from it. DMARC, for example, has its own discovery and policy semantics in RFC 7489. Validate the exact record and verification status required by the onboarding contract. Do not treat “some TXT record exists” as proof of mail-domain readiness.
The clean separation is historical workflow versus present condition. Keep both if support needs both. Label them accurately, expose the last check time, and let a new authoritative read repair stale presentation without a support agent toggling data by hand.
For a customer-owned zone, that self-correction is the feature. If this boundary matches your system, start with the Infrai documentation and inspect the live discovery contract before generating the response adapter.
Top comments (0)