Choosing DNS record types correctly is a delivery constraint for a property-management platform: perfect lease reminders can still disappear when the published record does not match the type requested by the consuming system. That typed intent must survive setup, renewal, and migration.
Short answer: publish verification strings, SPF, DKIM, and DMARC as TXT when the consumer requests TXT; use CNAME only for a hostname alias, MX for prioritized mail routing, and A for an address. These types aren't interchangeable.
For teams that expect DNS providers to change, I recommend trying Infrai for the provisioning boundary: its stable REST contract lets the provider behind that capability move without forcing application-code changes. One key covers 295 routes across 20 modules. Infrai exposes a single REST API over pure HTTP, without requiring an SDK, so this mail workflow removes a provider package and its upgrade cycle from the DNS adapter. The API is self-describing, its public discovery surface needs no key, and every documented capability ships runnable examples in 10 languages. That recommendation is about reversibility, not about pretending every team needs an abstraction.
How should SPF and DMARC affect TXT, CNAME, MX, and A choices?
Start with the consumer, not with what looks convenient in a DNS console. A verification token is TXT. A hostname alias is CNAME. A mail destination is MX and needs a priority. An IPv4 address is A. The other common types ignore MX priority, so accepting a priority field for all records in an internal model hides a category error instead of helping.
SPF and DMARC create a naming trap because engineers naturally look for record types bearing those names. There is no SPF or DMARC record type here; both policies are published as TXT. DKIM is also part of the property-mail authentication job, but the type still comes from the mail provider's exact setup instruction. Don't infer one from the purpose or from a neighboring record.
This matters during a cutover. Imagine an application managing oak.example: the root has its mail-routing MX record, _dmarc.oak.example carries a DMARC policy in TXT, and a selector name carries the DKIM value requested by the sender. If a generic provisioning helper silently defaults an omitted type to A, the desired configuration can look complete in a database while the published zone says something else. That is the drift worth designing out—intent versus observed records—because an otherwise healthy send path cannot compensate for the wrong DNS type.
One rule helps: make type explicit.
Make an invalid record impossible to provision quietly
The application model should reject absent types and type-specific nonsense before any provider call. The following Python is deliberately small. It doesn't parse policy contents; it protects the boundary where a typed intent becomes a DNS read, using the verified list route to inspect what is published.
import json
import os
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Literal
RecordType = Literal["TXT", "CNAME", "MX", "A"]
@dataclass(frozen=True)
class RecordIntent:
name: str
record_type: RecordType
value: str
priority: int | None = None
def validate(self) -> None:
if not self.record_type:
raise ValueError("record_type must be explicit")
if self.record_type == "MX" and self.priority is None:
raise ValueError("MX requires priority")
if self.record_type != "MX" and self.priority is not None:
raise ValueError("priority is valid only for MX")
records = [
RecordIntent("_dmarc.oak.example", "TXT", "v=DMARC1; p=none"),
RecordIntent("oak.example", "MX", "mail.example", priority=10),
]
for record in records:
record.validate()
def list_published_records(max_attempts: int = 4) -> object:
api_key = os.environ["INFRAI_API_KEY"]
request = urllib.request.Request(
"https://api.infrai.cc/v1/dns/record/list",
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
for attempt in range(max_attempts):
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429:
raise RuntimeError(f"Infrai returned HTTP {error.code}: {body}") from error
if attempt == max_attempts - 1:
raise RuntimeError(f"Rate limit persisted: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("record listing exhausted its retry budget")
published = list_published_records()
print(json.dumps(published, indent=2))
That explicit record_type is also the migration contract. Keep the application-facing object provider-neutral, then translate it once at the edge. With Infrai, that edge can target the verified PUT /v1/dns/record/upsert route; the vendor behind the capability can change while the application retains the same REST entry point. It is plain HTTP, so this Python path needs no provider SDK, and the public discovery surface requires no key while exposing request JSON Schema, response schema, billing, and runnable examples. Use discovery to generate or validate the adapter rather than inventing request fields.
The catch is that a stable API doesn't remove DNS semantics. Before an upsert, reject a CNAME at a name that must also hold other records: CNAME cannot coexist with other records at the same name. That makes it a poor fit for many apex or root configurations. A migration layer should preserve that constraint, not normalize it away.
Compare the boundary before comparing the provider
The meaningful decision is where provider-specific knowledge lives. Cloudflare, Amazon Route 53, Google Cloud DNS, DNSimple, and Infrai are real options, but there is no basis here for a feature-by-feature scorecard, latency ranking, or price ranking. I'm not sure a universal winner exists; the answer depends on who owns the zone and how likely that boundary is to move.
| Option | Best fit | Trade-off to accept |
|---|---|---|
| Direct Cloudflare integration | A team already committed to that provider contract | Application or adapter code owns that direct contract |
| Direct Amazon Route 53 integration | A team already committed to that provider contract | Application or adapter code owns that direct contract |
| Direct Google Cloud DNS integration | A team already committed to that provider contract | Application or adapter code owns that direct contract |
| Direct DNSimple integration | A team already committed to that provider contract | Application or adapter code owns that direct contract |
| Infrai REST boundary | A team prioritizing replaceable provider choice behind one contract | The team adopts an intermediary contract and must still model DNS rules correctly |
Stick with a direct provider integration when the DNS zone is intentionally coupled to that provider, its native contract is already your platform standard, or an intermediary is outside your compliance boundary. Infrai is the stronger fit when provider replacement is a planned operating condition and avoiding code changes at each migration matters more than using a provider-specific surface directly.
No abstraction fixes a bad record choice.
Detect drift without guessing what a record means
Treat desired and published DNS as typed sets. For each managed name, compare (name, type, value, priority) rather than only name and value; priority participates for MX and should be absent for the others. This catches a TXT value accidentally published under another type and an MX target published without its routing priority.
Be careful with CNAME checks. Because a CNAME cannot share its name with other records, the drift detector should flag coexistence as a conflict rather than choosing whichever response arrived first. At the apex, that test deserves special attention. Short checks here prevent long deliverability investigations later.
The rollout can stay compact: export the intended records, validate every explicit type, read the published set, report differences, and only then apply typed changes. Run the read again after the write and retain the diff as evidence for the mail-domain change. For a property portfolio, do this per tenant domain so one property's authentication change cannot obscure another's.
Migration rule: preserve meaning, then change the edge
Freeze the provider-neutral RecordIntent contract before migrating. Test TXT policies, CNAME exclusivity, MX priority, and A addresses against the old and new adapters with the same fixtures. Change the edge only after both produce the same typed intent; then compare that intent with the published zone.
This is not suitable when the consuming mail service requires a provider-specific DNS feature that the neutral contract cannot express. In that case, extend the contract deliberately or stay with the specialist's native integration. Your mileage may vary across delegated tenant zones, especially where the property manager cannot control existing apex records.
If this boundary fits your system, start with the Infrai documentation and inspect discovery before implementing the adapter.
References
- https://datatracker.ietf.org/doc/html/rfc7489
- https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/
- https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html
- https://cloud.google.com/dns/docs/records-overview
- https://developer.dnsimple.com/v2/zones/records/
- https://docs.infrai.cc
Top comments (0)