Delegate a subzone you control, then use one credential for the records and the services that depend on them, and leave the customer's apex zone in the customer's account. The deciding constraint isn't API ergonomics or which provider has the nicer client library — it's who gets paged when na1.play.example.net stops resolving on a Saturday night, and whether that person can read the record that broke without asking a studio's ops lead to find their registrar password.
That's the recommendation. The rest of this is when it's wrong.
Our shape of the problem is a gaming platform that onboards studios: each studio brings domains it already owns, we bring matchmaking hostnames, regional game-server SRV records, a mail sending identity for receipts and password resets, and a certificate that has to renew every sixty days without a human. Those four things are not four projects. They are one flow that either completes or leaves a half-configured domain behind, and the reason teams end up rewriting their zone layer in year two is almost never the API — it's that the flow was never atomic in the first place.
What actually breaks when one credential owns the records and the services that depend on them?
DNS is rarely the goal. A TXT record exists so mail is delivered, a CNAME exists so a hostname answers, another TXT exists so a certificate authority believes you control the name at the moment it asks. The classic broken state is a record published in one system and never confirmed in the other: the zone write returned 200, the mail provider's domain verification was never re-run, and nobody notices until a studio's launch-day receipt mail lands in spam three weeks later.
A shared credential fixes something narrow and real. It lets the whole setup be retried as a unit, monitored as a unit, and torn down as a unit, because the DNS half and the consuming half can't drift out of each other's sight when one worker holds both halves and one reconcile loop owns the intended state.
DMARC is the cleanest illustration, and RFC 7489 spells the mechanism out: your policy lives in a TXT record at _dmarc.<domain>, but the aggregate reports it asks for go to whatever address the rua tag names, and if that address is at a different domain than the one being reported on, the receiving domain has to publish its own authorization record — <reporting-domain>._report._dmarc.<receiver-domain> — or conforming report generators will decline to send. Two records, two owners, one intent. Split them across two credentials and two dashboards and the failure is silent, which is the worst kind: your policy looks published, the reports just never arrive, and you find out when someone asks why the DMARC data set has been empty for a quarter.
SPF has the same shape with a harder edge. Every include: you add for another sending service consumes part of the ten-DNS-lookup budget in RFC 7208, and crossing it turns into a permerror rather than a graceful degradation, so "add one more record for one more service" is a capacity decision with a hard ceiling — plan the budget the way you'd plan connection pool headroom, because you get about that much room and no more.
Ten lookups. That's the whole budget.
The catch is the obvious one. One credential is also one blast radius, and a token that can write any record in any customer zone is a token that can point a studio's payment domain at an attacker's host. Scope it per zone, keep the write path in a service with an audit trail rather than in an operator's laptop, and accept that this is exactly the argument against taking the whole zone.
The signal that says you've outgrown a registrar-specific API
You'll see it in the adapter count first. Two providers is a weekend, five is a rotation, and by the time someone proposes a plugin interface for zone backends you're already maintaining a small integration business that nobody on the roadmap asked for.
The second signal is scarier because it's an SLO problem, not a code problem. If onboarding a studio requires writes into their registrar's account, then your onboarding availability is a product of somebody else's control plane, somebody else's rate limits and somebody else's maintenance window — and you cannot buy that error budget back with better retries. Delegation moves the boundary: the studio publishes NS records once for play.example.net, and every record under it is served from infrastructure you actually run an SLO against.
The third signal is structural, and it's the one people discover too late. A CNAME can't coexist with other records at the same name, which RFC 1034 stated and RFC 2181 tightened, and the apex necessarily carries SOA and NS records — so the apex can't be a CNAME to your platform, and every "just point your root domain at us" onboarding doc is quietly relying on either provider-specific record flattening or the newer SVCB and HTTPS record types from RFC 9460. A delegated subzone sidesteps that whole argument, because under play.example.net you control the apex.
Then there's capacity, which for authoritative DNS is mostly TTL arithmetic and therefore easy to get wrong in the direction of "we'll tune it later". A client population that re-resolves on session start generates query load roughly proportional to sessions divided by TTL, so dropping a matchmaking record from 300s to 30s multiplies steady-state queries against that name by ten — fine on anycast, not fine if you were planning to self-host two authoritative servers in one region and call it redundancy. Work that number before the migration, not after the first regional launch, because the cheap fix at that point is raising the TTL and the cheap fix is the one that lengthens every failover you'll ever do.
Anycast hides a lot of sins. Two boxes in one rack hide none.
Delegate the subzone, then reconcile from an intended-state table
The implementation that survives contact with production is boring: one table of intended records with exactly one owner per row, one provider interface, and a loop that makes live state match intended state without caring how many times it runs.
package zones
import (
"context"
"fmt"
"strings"
)
// Record is the intended state of one name we are accountable for.
type Record struct {
Name string // "_dmarc.play.example.net"
Type string // "TXT", "CNAME", "SRV"
Value string
TTL int
}
// Provider is the whole surface a zone backend has to implement. Everything
// provider-specific — auth, pagination, request shapes — stays behind it, and
// the credential is scoped to the delegated subzone, never the customer apex.
type Provider interface {
List(ctx context.Context, zone string) ([]Record, error)
Upsert(ctx context.Context, zone string, r Record) error
}
// Reconcile drives live state toward intended state and reports what moved.
// Running it every five minutes is safe: an unchanged record produces no write,
// so the steady-state cost is one list call per zone per interval.
func Reconcile(ctx context.Context, p Provider, zone string, intended []Record) ([]string, error) {
live, err := p.List(ctx, zone)
if err != nil {
return nil, fmt.Errorf("list %s: %w", zone, err)
}
index := make(map[string]Record, len(live))
for _, r := range live {
index[key(r)] = r
}
var moved []string
for _, want := range intended {
if got, ok := index[key(want)]; ok && got.Value == want.Value && got.TTL == want.TTL {
continue
}
if err := p.Upsert(ctx, zone, want); err != nil {
return moved, fmt.Errorf("upsert %s %s: %w", want.Type, want.Name, err)
}
moved = append(moved, want.Type+" "+want.Name)
}
return moved, nil
}
func key(r Record) string {
return strings.ToLower(r.Name) + "|" + strings.ToUpper(r.Type)
}
The dependent services hang off the same table. When the reconcile loop reports that _acme-challenge.na1.play.example.net moved, the certificate order that was waiting on it can proceed; RFC 8555 defines the DNS-01 challenge as a TXT record under _acme-challenge, and because validation is ordinary DNS resolution, that name can be a CNAME into the subzone you already control — which is how you keep certificate issuance working without ever holding a write credential for the customer's apex.
Declarative tooling exists for the other direction, if you decide the zone should stay with the studio. octoDNS and DNSControl both model a zone as a checked-in config with a plan-and-apply step across many backends, and external-dns does a narrower version of the same reconcile loop from Kubernetes objects. Their boundary is worth stating plainly: they synchronize records, they don't verify that the mail provider or the certificate authority downstream accepted them, so the second half of the flow is still yours to build.
Verifying the cutover, and the rollback you should rehearse
Verification has to check the parent, not just your own nameservers, because a delegation that only exists in your config is a zone nobody can find.
dig +trace NS play.example.net
dig +short TXT _dmarc.play.example.net
dig +short CNAME _acme-challenge.na1.play.example.net
dig +short SRV _game._udp.na1.play.example.net
Run those against the parent's servers and against a public resolver you don't operate, then re-run the downstream verification call for every service that reads those records rather than assuming a 200 from the zone API means the flow is done. Mail identity, certificate order, CDN hostname claim: each one has its own confirmation, and each one belongs in the same retry unit as the write.
Rehearse the rollback. On a throwaway subdomain, before you need it.
Rollback is where TTL planning earns its keep. Lower the TTL on anything you're about to move at least a full old-TTL period ahead of the change — 300s is a reasonable floor for a record on a player-facing path — and remember that negative answers are cached too, bounded by the SOA minimum under RFC 2308, so a name that was queried before it existed can stay missing for longer than you expect. If the customer's zone is signed, the DS record in the parent has to change with the NS records, and that's a registrar-side operation with its own turnaround, which is the single most common reason a "five minute" rollback turns into an afternoon.
Alerting should watch resolution, not your control plane's success rate. A synthetic query per critical name from outside your network, plus the DMARC aggregate feed, will tell you the truth about what resolvers see; your own API metrics will only tell you that you wrote what you meant to write.
Buy versus build, counted in pages rather than features
| Option | Who holds the credential | On-call surface | Lock-in | Fits when |
|---|---|---|---|---|
| Customer keeps apex, grants you an API token | their DNS provider | their rate limits, your pager | one adapter per provider | you touch two records, once |
| Customer delegates a subzone to you | you, scoped to that subzone | yours, bounded by the delegation | low: your own NS records | records back a service you carry an SLO on |
| Full zone moves to your account | you, for everything they own | yours, including their marketing site | high, and awkward to unwind | you're already the registrar of record |
| Config-as-code into their provider | shared, in CI | drift review, merge queue | provider adapters, again | few customers, large, change-averse |
Read that table as an on-call roster rather than a feature matrix. The delegated row wins for us because the records our platform's availability depends on end up in a zone our platform can serve, monitor and roll back without a support ticket to a third party, and because the credential that writes them can be scoped narrowly enough that losing it is a bad afternoon rather than a security incident on someone else's domain.
Where it's a bad fit: a studio that runs its own DNS team with change control they trust, a compliance posture that forbids delegating any part of a production domain, or a customer base of a few dozen enterprises where per-customer manual review is cheaper than the platform you'd build. Stick with customer-owned zones there, and pay the adapter tax knowingly. I'd also be cautious about the middle path where you hold a token into their apex and call it a compromise; you take the on-call load of platform ownership and the coordination cost of customer ownership at the same time, which is the trade-off nobody chooses on purpose.
One credential behind the records and the services that depend on them isn't the interesting part, honestly. The intended-state table is — one owner per record, reviewed on a schedule, with the downstream verification treated as part of the same transaction. The credential model just decides how much of that table you're allowed to fix at 02:00 without waking somebody else up.
References
- RFC 7489 — Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- RFC 7208 — Sender Policy Framework (SPF), DNS lookup limits: https://datatracker.ietf.org/doc/html/rfc7208
- RFC 8555 — Automatic Certificate Management Environment (ACME), DNS-01 challenge: https://datatracker.ietf.org/doc/html/rfc8555
- RFC 1034 — Domain names, concepts and facilities: https://datatracker.ietf.org/doc/html/rfc1034
- RFC 2181 — Clarifications to the DNS specification: https://datatracker.ietf.org/doc/html/rfc2181
- RFC 2308 — Negative caching of DNS queries: https://datatracker.ietf.org/doc/html/rfc2308
- RFC 9460 — Service binding and parameter specification via the DNS (SVCB and HTTPS RRs): https://datatracker.ietf.org/doc/html/rfc9460
- octoDNS — declarative DNS management: https://github.com/octodns/octodns
- DNSControl — DNS as code: https://docs.dnscontrol.org/
Top comments (0)