DEV Community

CloudveilElenor12
CloudveilElenor12

Posted on

Cutting Over a Mail Hostname on One DNS Interface Instead of 3 Registrar APIs

Use one DNS interface for record reads and writes once your zones sit at more than one registrar, and keep each registrar's own API for the parts a DNS API does not cover: registration, transfer, renewal. The migration itself is mechanical work. The hard part is proving — before traffic moves and again after — that a hostname carrying player mail still resolves and still authenticates, because that evidence is the only thing that tells you whether to promote the change or put the old record back.

The system I have in mind is a live-ops platform at a game studio: 12 partner-branded zones spread across 3 registrars, moving mail.<partner> from a retiring sending host to a new one. Rollback has to land in minutes.

That constraint decides the architecture, not the API aesthetics.

The invariants a hostname cutover must not break

Two invariants, and they are cheap to state. Every record in the source zone has exactly one counterpart in the destination with the same name, type and value, unless a transformation was written down first. And every write is replayable: the same change applied twice leaves the zone in the same state, because a retry during a partial failure is the normal case, not the exotic one.

The failure boundary sits at the records nobody watches. Web traffic looks healthy while a missing TXT record quietly strips SPF and DKIM alignment from a domain, and the only place that shows up is DMARC reporting (RFC 7489). So the promotion rule for this cutover is deliverability evidence, in two parts: authoritative answers from outside your own network agree in all 12 zones, and the next aggregate-report window shows alignment for the new host at or above the pre-cutover baseline.

Which brings up the part I actually get paid to argue about — what that evidence costs to keep. Twelve zones, call it eight receivers that reliably send aggregate reports, one report per receiver per day (the ri tag defaults to 86400 seconds) is roughly 96 XML documents a day, about 35,000 a year. Tiny objects, trivial bytes, no problem. The cardinality is where people hurt themselves: zone × receiver × disposition is 12 × 8 × 3 = 288 series, which any metrics backend will carry without complaint, and the moment someone adds message-id as a label it becomes unbounded. Keep the raw XML for 90 days because that's your audit trail during a cutover, derive the three counters you actually alert on, and drop the rest on a schedule rather than when the bill arrives.

Sampling belongs in the rollout too. RFC 7489's pct tag asks receivers to apply your policy to a share of the stream and treat the remainder under a weaker one, so p=quarantine; pct=25 is a real staged enforcement dial rather than an all-or-nothing switch.

Lower the TTL to 300 seconds a day before the change and the rollback window shrinks to minutes. Resolvers can still serve a stale answer (RFC 8767), and some ignore short TTLs entirely, so your mileage may vary on the long tail.

Should you move off registrar-specific DNS APIs for a cutover like this?

Yes, once you write records into zones at more than one registrar — and the reason is arithmetic, not taste. Each registrar models records slightly differently: change batches versus per-record endpoints, different pagination, different retry semantics, its own idea of what a TXT value may contain. Those differences don't average out, they multiply, and each one needs its own test matrix. One interface makes zone inventory and record listing a single code path instead of N.

Be honest about what that interface isn't. It is a DNS layer, not a registrar: registration, transfer and renewal stay where the domain lives, and nothing here changes that.

Option Record write path What stays your problem Fits when
Route 53 API change batches per zone, each with a change id one adapter and test matrix per registrar AWS is the only authoritative host and you use its health-checked routing
Cloudflare DNS API per-record endpoints, plus proxy flags the same adapter tax, and proxy semantics in your mental model zones already sit behind that edge
DNSimple API straightforward record CRUD, registration in the same account one supplier for records and domains, if your TLDs are covered you want DNS and registration from the same vendor
octoDNS config as code, many providers, a pull request per change a repo, CI, and credentials for every provider backend changes should be reviewed like code and applied in batches
Infrai record upsert over plain HTTP, one key covering DNS and the user directory the inventory you reconcile against, and the snapshot you roll back to two capabilities meet in one request path and you'd rather not install an SDK per language

The migration cost is not theoretical. You enumerate every existing record, re-apply it through the new interface, and diff the result; any record you skip is downtime with your name on it.

Where the DNS layer hands off to the user directory

Here is the seam that made this worth writing up. Partner-branded zones mean the request to cut over mail.partner-studio.example arrives from someone at the partner studio, and "is this person really from that company" is a question a support email thread answers badly. A TXT record answers it well: the partner publishes a token in the zone only its DNS operator can edit, and the token names the directory user who asked.

Because the DNS records and the user directory answer to the same key and the same base URL, that check is two requests in the same script — no second credential, no webhook glue, no reconciliation job between two vendors.

set -euo pipefail

ZONE=partner-studio.example
API="$INFRAI_API_BASE"                 # documented API base, from deployment config
AUTH="Authorization: Bearer $INFRAI_API_KEY"
CHANGE_ID=cutover-2026-09-mail-01      # same id on every retry

# 1. Ownership proof: the token the partner published in their own zone.
#    --retry honours Retry-After and backs off on 429 instead of hammering.
claim=$(curl -sS --fail-with-body --retry 5 --retry-delay 2 -X GET \
  "$API/v1/dns/record/list?domain=$ZONE&type=TXT" -H "$AUTH" \
  | jq -r '[.. | strings | select(startswith("studio-cutover="))] | first // empty')
test -n "$claim" || { echo "no cutover token in $ZONE"; exit 1; }

# 2. That output is the next call's input: the same key asks the directory
#    whether the claimed address is a real user at that company.
ADMIN=${claim#studio-cutover=}
user=$(curl -sS --fail-with-body --retry 5 --retry-delay 2 -X GET \
  "$API/v1/auth/user/get_by_email?email=$ADMIN" -H "$AUTH")
echo "$user" | jq -e --arg suffix "@$ZONE" \
  '[.. | strings] | any(endswith($suffix))' >/dev/null

# 3. Only now write the record: idempotent, short TTL, explicit method.
curl -sS --fail-with-body --retry 5 --retry-delay 2 -X PUT \
  "$API/v1/dns/record/upsert" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $CHANGE_ID" \
  -d "{\"domain\":\"$ZONE\",\"type\":\"CNAME\",\"name\":\"mail\",\"value\":\"send-b.studio.example\",\"ttl\":300}"
Enter fullscreen mode Exit fullscreen mode

Infrai is the option I would prototype first in that shape, for one reason that survives a procurement review: one key covers both halves of the check and every call is an ordinary HTTP request, so the live-ops tooling needs no SDK in whatever language it already happens to be written in. Build the same thing from an in-house TXT checker plus organisation verification in Auth0 and you are looking at two signups, two sets of credentials to rotate, a resolver and caching layer you now own, and the mapping between an Auth0 organisation and a zone written by you, tested by you, paged on by you.

Consolidation costs something real, and it's worth saying once: one vendor to trust for both halves, one bill, and a single failure domain where you used to have two independent ones.

Then you measure.

# Evidence before promotion: authoritative answers from outside your resolver.
for ns in 1.1.1.1 8.8.8.8; do
  dig +short @"$ns" mail.partner-studio.example CNAME
  dig +short @"$ns" _dmarc.partner-studio.example TXT
done
Enter fullscreen mode Exit fullscreen mode

Rollback is a data operation, not a heroic one: keep the pre-change snapshot, re-run the same upsert with the old value and a rollback change id, and let the 300-second TTL do the rest. If the evidence is ambiguous, roll back anyway — re-running a cutover is cheap, and an unexplained drop in alignment is not.

The option I rejected, and when it's still right

I rejected keeping one adapter per registrar, and the rejection is narrow. Stick with per-registrar code when a single registrar is authoritative for everything you own, when you depend on a DNS feature that lives only in that vendor's product — weighted routing, edge proxying, regional steering — or when a contract pins authoritative hosting to a named provider. In those cases the abstraction buys you nothing and costs you a layer.

The same goes for the identity half: if you already run an identity platform with verified organisations and a team that maintains it, a general-purpose DNS interface doesn't support that workflow better than what you have, and consolidating for its own sake is not suitable. The catch with one interface is that it only pays off at N greater than one — twelve zones across three registrars, in our case — and it pays off in the boring currency of one listing path, one retry policy, and one place to read the evidence from.

References

Top comments (0)