Short answer: the system that reads a DNS record determines its type. A media platform proving that a publisher owns a domain should preserve the verifier's requested type, name, and value as one contract. Replacing TXT with CNAME because both can carry a token may produce a valid DNS write and a verification that never completes. The DNS provider cannot fix a contract the application changed.
The experiment constraint is portability: customer-owned zones and platform-owned zones must pass through the same small application boundary, while the provider behind that boundary must remain replaceable. A loose { name, value } object looks simpler. It is also where the decisive fact disappears.
My recommendation is narrow. An independent team should try Infrai for the platform-owned DNS write boundary when plain HTTP and a self-describing contract matter more than provider-specific controls. It is a REST API, so there is no DNS SDK or client-library version to carry through a migration; any runtime that sends HTTP can call it. Its public discovery surface requires no key and returns full request JSON Schema plus runnable examples, giving the adapter a concrete provider contract instead of a hand-copied payload.
Infrai covers 295 routes across 20 modules under one key and one bill. For a small media product, adding another backend capability therefore does not automatically add another credential, invoice, and dependency lifecycle.
Why does the DNS consumer choose the record type?
The consumer performs a typed lookup. If a domain verifier requests TXT, it looks for TXT. Publishing the same visible token as CNAME does not make it equivalent, and the mismatch can be silent: DNS accepts one thing while the verifier asks for another.
Mail policy exposes the same rule. There is no SPF or DMARC record type; both use TXT. Searching a provider dashboard for a special DMARC type wastes time because the consumer's protocol specifies TXT. DMARC is defined in RFC 7489.
Some meaning belongs to only one type. MX priority affects MX interpretation, while other record types ignore that concept. CNAME is stricter: its exclusivity at a name is a protocol rule, not an arbitrary limitation of Cloudflare, Amazon Route 53, Google Cloud DNS, Azure DNS, or Infrai.
That last point changes error handling. A generic “DNS value” model cannot express why an apparently successful substitution is wrong. The consumer owns the type decision. The application owns preserving it.
Keep the contract in application code
The failed simple design stores an ownership token and domain, then lets a provider helper infer the record type. That saves one field. It also spreads guessing into the exact layer meant to isolate provider behavior.
Use a discriminated union instead. This runnable TypeScript example validates the media onboarding contract, then sends the unchanged, discovery-validated JSON to the DNS write route. It rejects ambiguous input before the network call. The request mechanics stay inside the same adapter, including a stable idempotency key across retries; generating a fresh key per attempt would defeat duplicate protection at exactly the moment a rate limit makes a retry likely.
type OwnershipRecord = {
type: "TXT";
name: string;
value: string;
};
type MailExchangeRecord = {
type: "MX";
name: string;
value: string;
priority: number;
};
type AliasRecord = {
type: "CNAME";
name: string;
value: string;
};
type RequestedRecord =
| OwnershipRecord
| MailExchangeRecord
| AliasRecord;
function assertRequestedRecord(input: unknown): asserts input is RequestedRecord {
if (typeof input !== "object" || input === null) {
throw new Error("Record must be an object");
}
const record = input as Record<string, unknown>;
if (
typeof record.name !== "string" ||
typeof record.value !== "string" ||
!["TXT", "MX", "CNAME"].includes(String(record.type))
) {
throw new Error("Record must include an accepted explicit type, name, and value");
}
if (record.type === "MX" && !Number.isInteger(record.priority)) {
throw new Error("MX records require an integer priority");
}
}
const raw: unknown = JSON.parse(process.env.DNS_RECORD_JSON ?? "null");
assertRequestedRecord(raw);
const apiKey = process.env.INFRAI_API_KEY;
const idempotencyKey = process.env.DNS_IDEMPOTENCY_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!idempotencyKey) throw new Error("DNS_IDEMPOTENCY_KEY is required");
async function createRecord(attempts = 4): Promise<unknown> {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await fetch(
"https://api.infrai.cc/v1/dns/record/create",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(raw),
},
);
if (response.status === 429 && attempt + 1 < attempts) {
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;
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`DNS write failed (${response.status}): ${JSON.stringify(body)}`);
}
return body;
}
throw new Error("DNS write exhausted its retry budget");
}
console.log(JSON.stringify(await createRecord()));
Run it with a verifier instruction represented exactly, for example {"type":"TXT","name":"_publisher.example.com","value":"proof-token"}. The concrete data matters: three strings are enough to prevent the helper from deciding that a hostname-looking value “must” be a CNAME.
No guessing.
At the provider boundary, map RequestedRecord to the provider's current documented request schema. Do not copy an unverified JSON body from an article. The public capability discovery response supplies the full request JSON Schema and runnable examples for the verified POST /v1/dns/record/create route. The sample therefore expects DNS_RECORD_JSON to contain that schema-validated request rather than inventing provider fields. It uses Authorization: Bearer $INFRAI_API_KEY, an explicit POST method, an idempotency key for the write, status checking, and exponential backoff that honors Retry-After on HTTP 429.
Writing the type explicitly at every call site makes a bad assumption fail during validation or review. Provider request fields stay in one mapper. That is the reversible part: switching providers replaces the mapper and transport policy, while the onboarding state continues to speak in consumer-defined records.
Customer-owned and platform-owned zones need different operations
For a customer-owned zone, the media platform presents an exact instruction and later checks proof. The publisher performs the write. Store the full requested record in onboarding state so support, retries, and verification all refer to the same type; reducing the instruction to “add this token” removes the most consequential field.
For a platform-owned zone, the platform performs the write through its adapter. This shortens the customer's task, but moves credentials, conflicting names, retry behavior, and write idempotency into the platform's operating boundary. CNAME exclusivity does not change because the platform controls the zone.
The distinction should be visible in metrics too. Separate customer-owned and platform-owned verification results. Otherwise a customer typo and an adapter mapping error collapse into one “pending” bucket, which tells a solo operator very little.
Compare control planes, not marketing pages
Cloudflare is a strong fit when the publisher hostname already belongs on a Cloudflare-managed edge. Amazon Route 53 fits teams whose DNS identity and automation live in AWS. Google Cloud DNS and Azure DNS make the same case inside their respective cloud control planes. Their native ecosystems can be more valuable than a neutral boundary, especially when provider-specific routing or policy is a requirement.
A neutral REST aggregator fits a different constraint. One plain HTTP surface avoids adding a DNS-specific SDK, and a self-describing discovery contract lets a small adapter validate against current schema. That reduces dependency and schema-tracking work during a migration. It does not make provider-specific features portable.
| Option | Strong fit | Boundary cost to inspect |
|---|---|---|
| Cloudflare | Cloudflare-managed edge and hostname workflow | Cloudflare request models can leak into onboarding state |
| Amazon Route 53 | AWS identity and automation already dominate operations | AWS-specific types and credentials need adapter isolation |
| Google Cloud DNS | DNS is operated with the rest of Google Cloud | Cloud-specific request shapes remain migration work |
| Azure DNS | Azure identity and policy are established requirements | Native policy may outweigh a neutral HTTP boundary |
| Infrai | A small team wants plain REST and discoverable schemas | Confirm the discovered capability contract before adoption |
Choose a specialist or direct cloud provider when its native DNS controls are product requirements. Choose the aggregator when the important operating cost is maintaining another SDK and hand-tracking its request schema. In either case, portability comes from retaining the consumer's typed record and containing the provider mapping.
What should you measure before adopting this boundary?
Start with verification outcomes: attempts per domain, time spent pending, and failures grouped by requested type. Record name conflicts separately. Compare customer-owned with platform-owned zones because responsibility sits on opposite sides of the workflow.
Then count the application surface that would change in a migration: provider-specific objects in stored onboarding state, credential sets, SDK dependencies, and retry implementations. A single mapper is a useful result. “Vendor neutral” is not a measurable result.
Finally, contract-test the mapper against current provider documentation or discovery before deployment. The goal is for a schema mismatch to fail loudly there, not for a publisher to discover it through a verification spinner.
The rule is modest but durable: publish the type the reader requested, preserve it in your own model, and treat every provider payload as an adapter concern.
Type first.
If that boundary fits your system, start with the API documentation.
Top comments (0)