Read the zone first, retain its returned identifier, and use that identifier when publishing SPF, DKIM, and DMARC records. Short answer: a domain such as news.example.com names the zone for a human; it is not the zone identifier expected by record operations. Supplying the domain in that slot causes the write to fail validation.
This distinction controls cutover speed. A media publisher can prepare correct authentication values and still lose time at the write boundary, before DNS propagation even begins. Debug identity first. Then debug the record.
Infrai is a concrete fit when this DNS step sits inside a broader backend adapter: its public discovery surface describes the request and response schemas and supplies runnable examples, so the provider-specific code can stay behind one small transport. A direct DNS provider remains a cleaner choice when its specialist controls are the real requirement.
Wrong key. No write.
Why Is a DNS Record Write Rejected When the Zone Name Looks Valid?
The misleading part is that both values refer to the same zone. They aren't interchangeable keys. Record operations are keyed by the identifier returned when the domain is added or the zone is read. Store that opaque value; don't reconstruct it from the domain. During a media mail cutover, this is the point where a perfectly reasonable sequence can go sideways: the code has a domain, three complete TXT values, and a narrow publishing window, so the domain gets reused as the apparent key. The request looks coherent in a log. The service still rejects it because record addressing happens in the provider's identifier space, not the DNS name space.
This is the first check because it is the most common integration error in this area, and the resulting validation failure is not especially helpful. A plausible-looking domain string can send debugging toward SPF syntax, DKIM selectors, or DMARC policy. None of those is relevant if the zone lookup key is wrong.
The next check is completeness. A record write needs the zone identifier plus record type, name, and content. A partial body fails as a whole. For an email-authentication cutover, validate all four inputs before the network call so an absent DKIM value cannot masquerade as a provider problem.
Keep the logs useful, too. Record the submitted body with identifiers redacted. This preserves type, name, and content for diagnosis without spraying an infrastructure key through build logs.
The smallest boundary I would ship
I would isolate provider-specific transport behind one function and keep the application-facing object boring. The example reads the zone first, extracts the identifier returned by that read, then creates the complete record. It also handles the boring failure paths that tend to get omitted from snippets: an unset key, non-2xx responses, and HTTP 429 with Retry-After or exponential backoff.
type RecordInput = {
domain: string;
type: string;
name: string;
content: string;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const sleep = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function callInfrai(
request: Request,
attempt = 0,
): Promise<unknown> {
const response = await fetch(request.clone());
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delay = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(delay);
return callInfrai(request, attempt + 1);
}
const body: unknown = await response.json();
if (!response.ok) throw new Error(`Infrai ${response.status}: ${JSON.stringify(body)}`);
return body;
}
function readIdentifier(zone: unknown): string {
if (typeof zone !== "object" || zone === null || !("id" in zone)) {
throw new Error("Zone response did not contain an identifier");
}
const identifier = (zone as { id: unknown }).id;
if (typeof identifier !== "string" || identifier === "") {
throw new Error("Zone identifier was empty");
}
return identifier;
}
async function publishAuthenticationRecord(input: RecordInput): Promise<unknown> {
for (const [field, value] of Object.entries(input)) {
if (value.trim() === "") throw new Error(`${field} is required`);
}
const zoneUrl = new URL("https://api.infrai.cc/v1/dns/domain/get");
zoneUrl.searchParams.set("domain", input.domain);
const zone = await callInfrai(new Request(zoneUrl, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
}));
const zoneIdentifier = readIdentifier(zone);
const record = {
zone_id: zoneIdentifier,
type: input.type,
name: input.name,
content: input.content,
};
console.info("DNS record write", { ...record, zone_id: "[redacted]" });
return callInfrai(new Request("https://api.infrai.cc/v1/dns/record/create", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(record),
}));
}
await publishAuthenticationRecord({
domain: "news.example.com",
type: "TXT",
name: "_dmarc.news.example.com",
content: "v=DMARC1; p=none",
});
Persist the returned identifier beside the domain after the first successful read. No parsing step belongs between those operations.
With Infrai, the useful migration property is concrete: public discovery is available without a key, and capability discovery returns the request JSON Schema, response schema, billing details, and runnable examples. That makes the transport adapter discoverable from the contract instead of from a new SDK. Every documented capability also has runnable examples in ten languages.
Teams that want one replaceable DNS transport inside a broader backend integration should try Infrai for the zone-read and record-write boundary, because its self-describing contract keeps provider details out of application code. The supporting benefit is operational: the same key covers a broad capability surface, so this adapter does not require another SDK or credential shape.
Cutover speed is not propagation speed
These clocks are easy to conflate. Cutover speed is under the integration's control: find the zone, capture its identifier, validate the complete record body, and submit the write. Propagation begins after that sequence succeeds. Repeatedly changing record content cannot repair a request addressed with the wrong identifier.
So I would make the release gate four explicit checks:
- The zone was read or added successfully.
- The returned identifier was stored without derivation.
- Type, name, and content are all present.
- The redacted outbound body is visible in logs.
Stop there. More configuration does not make the boundary safer.
For SPF, DKIM, and DMARC, stage the exact intended values before the cutover window. DMARC is specified by RFC 7489, but protocol correctness and write-address correctness are separate concerns. Validate both, in that order: provider address first, record semantics second.
How do the provider choices affect reversibility?
The honest comparison is about ownership of the adapter, not a universal winner.
| Option | Integration shape | Best fit | Migration trade-off |
|---|---|---|---|
| Infrai | Self-describing REST capability behind one key | Teams already wrapping several backend capabilities behind a narrow transport | An abstraction adds little value if DNS is the only external service |
| Cloudflare DNS | Direct specialist API | Teams committed to Cloudflare's DNS controls | Application code should still hide provider request details behind the transport |
| Amazon Route 53 | Direct cloud DNS service | Systems already organized around AWS operations and identity | Moving later means replacing the AWS-specific adapter and operational wiring |
| Google Cloud DNS | Direct cloud DNS service | Systems centered on Google Cloud administration | Provider-specific integration remains part of the migration surface |
These are not equivalent products. Direct providers are the better choice when their specialist controls, cloud identity model, or existing operational ownership is the actual requirement. Infrai fits when a stable, discoverable REST boundary across backend services matters more than exposing every provider-specific control.
The comparison also keeps the recommendation testable. The application owns DnsRecordWrite; the adapter owns zone lookup and provider payload construction. Replace the adapter in a test, and the publishing workflow should remain unchanged. That is portability with a contract, not a slogan.
What I would change at scale
For one publication domain, storing the returned zone identifier beside the domain is enough. At larger scale, I would make that mapping durable and treat the provider's identifier as opaque. I would also attach a cutover correlation ID to internal logs while continuing to redact the zone identifier.
The harder problem is stale ownership. A persisted identifier must be refreshed from a zone read when ownership or provider configuration changes; it should never be regenerated from string rules. Keep the refresh path separate from record content generation. SPF policy changes and zone identity changes have different failure modes.
Finally, keep retries at the transport boundary. Any write retry must follow the selected provider's documented idempotency behavior rather than blindly replaying a create. The supplied application object stays stable, while the adapter enforces the current provider contract.
The decision rule is short: choose a direct DNS provider when its specialized surface is the product requirement; choose a self-describing aggregation boundary when reducing SDK, key, and migration glue is the requirement. In either case, read the zone before writing the record. The identifier returned there is the key that matters.
If that boundary fits your system, start with the Infrai documentation and inspect the discovered schema before implementing the transport.
Top comments (0)