DoDomain
Guides

Domain Connect field notes: two providers end to end, every bug we hit

What it actually took to ship signed one-click DNS applies over Domain Connect — from the template registry to a real record on a real provider — including the spec's silence on parameter order, a provider's exponentiation bug, chunked public keys, and why a 200 on a template probe is not a working integration.

Domain Connect is the open protocol for one-click DNS: a service publishes a template of the records it needs, a DNS provider chooses to serve that template, and a user approves one screen at their provider instead of pasting records by hand. The protocol has an IETF draft, a public template registry, and a list of providers that implement it. It also has a reputation, mostly in GitHub issues, for being harder to ship than the spec suggests.

These are our notes from doing it. DoDomain's two templates were merged into the registry on 2026-07-24, and a real signed apply — the provider writing the record, DoDomain verifying it on the zone's authoritative nameservers, the session finalizing and the webhook delivering — has been proven on one provider, Glauca HexDNS, for the TXT template on 2026-08-06 and for the CNAME template on 2026-08-12. Two more providers serve the templates today without a proven apply, three large ones do not serve them at all, and the nightly-measured state of that is a page of its own. Every claim below traces to a dated record in our repository; nothing is rounded up.

1. The registry is not the provider

The first thing the protocol does not say loudly enough: getting a template merged into the registry and getting a provider to serve it are two separate events, and the second is entirely the provider's decision. On 2026-06-25, before our templates existed, we probed IONOS's Domain Connect API with a handful of registry templates: the reference exampleservice.domainconnect.org templates, Google's gmail-setup and domain-verification, Squarespace's website — all 200, onboarded. The same day, against a Cloudflare-hosted zone we control, every one of those templates answered 404. Same registry, opposite policies: IONOS onboards broadly, Cloudflare hand-curates.

After our templates merged (Domain-Connect/Templates#1436, a resubmission of #1376), the timeline of who served them was: Domain Chief on 2026-07-28, four days after the merge, apparently indexed straight from the registry; Glauca HexDNS on 2026-07-29; Cloudflare on 2026-09-02, nearly six weeks later; and GoDaddy, IONOS and 1&1 not at all as of this writing. The rule we took from it: a connect flow must probe the provider for the specific template before it shows a one-click button, and must fall back to guided manual records when the probe says no — which is what the flow does, and why the button is honest. The probe itself needs a control: a 404 on our template means nothing unless a widely-served template on the same API answers 200, so every probe carries one.

2. Discovery values are not always a bare host

The spec's discovery step reads a TXT record at _domainconnect.<zone> whose value is the provider's Domain Connect API host. Cloudflare publishes api.cloudflare.com/client/v4/dns/domainconnect — a host plus a path prefix. Our first implementation rejected it as malformed (2026-06-25). The fix accepts a host with a safe path prefix and still refuses a scheme, userinfo, path traversal or a query string; it has a regression test. If you write a discovery client, expect the path.

3. The public key has to be published in pieces

Templates that set syncPubKeyDomain require every synchronous apply to be signed, and the provider fetches the public key from a TXT record at <key>.<syncPubKeyDomain>. Two things about that record are easy to get wrong, and we got both wrong first:

  • One TXT string is capped at 255 characters, and a base64 RSA public key is longer. The spec wants the key split across p= fragments the provider reassembles in order, alongside a=RS256 and d= values. Our first record was one blob; Cloudflare's onboarding review caught it. The chunked record went live at _dck1.dckeys.dodomain.io on 2026-07-21.
  • The syncPubKeyDomain label cannot start with an underscore. We had chosen _dckeys.dodomain.io; the registry's online template editor rejects an underscore label there, so it became dckeys.dodomain.io (the _dck1 key label in front of it is fine — that one is conventional). Same key pair, key id unchanged, old record deleted.

Rotation is a new _dck2 record beside the old one and an environment swap; nothing in the templates changes.

4. What is signed is universal; where sig and key sit is not

This is the bug that cost the most, and it is a real gap in the specification rather than a misreading of it.

What is signed is settled: the spec's Security Considerations say the signature "will be generated on the full query string only, excluding the sig and key parameters", and the provider-side reference implementation (Domain-Connect/DomainConnectApplyZone) filters both parameters by name before verifying. We shipped, on 2026-07-12, an apply URL that signed key= into the payload. Every conformant provider would have rejected it before the consent screen; we found out on the first live attempt and fixed it on 2026-08-06.

Where the two parameters go is where the spec is silent, and two live providers filled the silence in mutually exclusive ways:

  • Glauca HexDNS verifies positionally. Their verifier takes everything before the last &sig= in the raw query string as the signed payload. Emit key before sig and key=_dck1 lands inside their payload: 403 Invalid request signature, reproduced live on 2026-08-06. Moving only key after sig on the already-signed URL — no re-signing — reached their consent page.
  • Cloudflare requires sig to be the last query parameter. That is verbatim on their Domain Connect page ("Signature: Required. It also must be the last query parameter."), and it is the exact opposite.

No single URL satisfies both, so the emission order is now an argument chosen per DNS provider — the provider's own providerId from its settings document, not our service id — with sig-then-key as the default that Glauca proved, and key-then-sig only for cloudflare.com (shipped 2026-08-12). The payload is byte-identical under both orders, so the same signature is valid either way and name-filtering providers accept both. Worth saying plainly: the Cloudflare order is unit-tested and has never run against a live Cloudflare zone, because no apply has (section 11).

5. A provider's login wall proves nothing

While debugging the signature we learned to distrust a comforting symptom. Domain Chief answers a valid apply URL, a tampered one, and one with no sig or key at all with the same 302 to its login page — it validates only after the user authenticates. So "the apply URL renders the provider's sign-in" is not evidence that the signature is right. Our own offline verification against the published _dck1 key proves the bytes we emit are correct; only an authenticated apply on a zone we control proves a provider accepts them. That became our written definition of "working": the provider serves the template and a real apply has been verified against the authoritative DNS of a zone that provider manages. Nothing gets called working before that.

6. Zones, not apexes

The proof zone is dc.mrneon.online, a subzone delegated from mrneon.online to Glauca's nameservers — deliberately, because it is the shape a real customer has when a subdomain is hosted somewhere other than the registrable domain. It exposed three bugs of ours in one afternoon (2026-08-06), all the same mistake:

  • Tier routing on the apex. Provider detection looked up the nameservers of mrneon.online and routed the session by the wrong provider. Fix: detect on the zone that actually owns the record, the nearest delegation cut.
  • The recipe pre-flight compiled against the apex, so every delegated-subzone session for the TXT template failed with a 400 before an apply URL existed.
  • Verification asked the apex's nameservers, which do not carry the subzone's records, and read a record the provider had just written as absent.

The shared lesson is a function that answers "which zone owns this name" — floored at the registrable apex, never assumed to be it — and using it in all three places. We had the apex helper already; it was being used to answer a question it does not answer.

7. Then the provider's own bug

With ours fixed, the TXT template round-tripped on 2026-08-06: signed apply accepted, consent screen rendered our template and the exact record, the provider wrote _dodomain-challenge, all four authoritative nameservers served it within about 15 seconds, verification passed on the first call, the session finalized, the usage meter moved, the session.completed webhook delivered.

The CNAME template did not. Every CNAME apply at Glauca returned 500. The cause was in HexDNS itself — a missing comma in the apply code turned ttl=record.ttl, **record.data into ttl=record.ttl **record.data, exponentiation, on the CNAME and AAAA branches. It was tracked upstream as AS207960/hexdns#9; the maintainer closed it the same day (2026-08-06T14:56Z), and the fix was deployed when we retested: on 2026-08-12 the CNAME apply returned no 500, the consent screen showed cnametest / CNAME / 3600 / status.getuptimely.com, all four nameservers served the record, and the session verified and finalized. That is the day both shipped templates became proven.

A provider serving your template can still be unable to apply it. Only the apply proves the apply.

8. The zone is published asynchronously

One minute after the CNAME apply, all four authoritative nameservers answered NXDOMAIN for the new name while the provider's control panel already showed the record. It was live on all four about 75 seconds after the apply. Read an immediate post-apply miss as a failed apply and you will chase a bug that is not there — check the panel first.

Probing that early has a second cost: it seeds negative caches. Two public resolvers we asked at the one-minute mark were still returning nothing four minutes later, while a third had the record. That is cosmetic for DoDomain, whose verification reads the authoritative nameservers and passed regardless, but it is exactly the sort of thing that makes an end user think their record "did not work" when a debugging tool asks a public resolver too early. It is also why the free DNS lookup shows the authoritative answer and public resolvers side by side.

9. Applying over an existing record

HexDNS does not tolerate a template landing on top of a record that already exists: re-applying the TXT template over an existing _dodomain-challenge raises a KeyError and answers 500. Our operational rule is to delete the record in the panel before any re-apply; verify-only retests are safe and idempotent. The proof records were deliberately left in place. If your test plan re-runs an apply against the same zone, expect this class of failure and design the conflict handling into the template — the spec's txtConflictMatchingMode exists for it, and providers honour it unevenly.

10. Transient provider hops, and how slow "fast" is

Glauca's settings endpoint took between 987 and 1,944 ms across the proof runs — the slowest provider hop we have measured, and two-thirds of the budget the one-click readiness probe has before the hosted page gives up and shows manual records. One CTA click during the run bounced on a transient fetch failed from the provider and the next succeeded, so the apply-start route now retries its provider hops exactly once on a transient network failure (2026-08-06). Budget the discovery and probe chain, cache what you can within a session, and never let a slow provider decide that a supported provider is unsupported.

11. Cloudflare: served, not proven

On 2026-09-02 our nightly watch found Cloudflare answering 200 for both templates — the third provider ever, and the first large registrar. A same-day hand probe confirmed it was real: a bogus service id on the same API still 404s, so it is not a blanket-200 endpoint. It is also the provider that needs the key-then-sig order from section 4, which has never run live. So Cloudflare is served, not proven: no apply has happened on a Cloudflare-managed zone, the one-click button keeps its fail-closed probe behaviour there, and DoDomain does not describe itself as one-click on Cloudflare until a real apply lands. (Cloudflare-hosted zones get one-click today anyway, through DoDomain's own OAuth path, which does not involve Domain Connect at all.)

The detection itself nearly went unnoticed, which is its own lesson: the watch's issue-opening step shelled out to a CLI the CI runner image does not ship, exited 127 on 2026-09-01 and 2026-09-02, the red was read as tooling noise, and the one event the watch exists to deliver was dropped. It now uses the platform's own scripting action, a build gate refuses any shell-out to that CLI under the workflows directory, and the re-run opened the issue.

12. Templates drift after they are served

A provider caches the templates it serves. When the registry copy moves to a new version and the provider keeps serving the old one, that provider's users get the stale records — Cloudflare served a five-week-stale template for another vendor in 2026 (Domain-Connect/Templates#1570). A served template's 200 body carries its version, so the nightly watch compares the version each provider serves against the registry master and marks a mismatch as drift on the state page, in red. Bake the version check into whatever monitors your templates; "served" is a state that can quietly rot.

13. One record per template is not enough

The protocol applies one template per approval, and our two merged templates each write exactly one record. A production custom-domain flow usually wants two — a CNAME plus an ownership TXT — and an email setup wants three or four. The template model does allow it: records in a template belong to optional groups, and the apply's groupId parameter selects which groups to apply. So as of 2026-09-03 DoDomain's templates are data, a session's whole record set is compiled to exactly one template, and when the set fills only some of a template's groups the groupId list rides inside the signed payload. Three multi-record templates (CNAME + TXT, apex A/AAAA + TXT, SPF/DKIM/DMARC — never MX) are prepared and byte-locked to the in-code registry by a test, and not yet submitted: a provider applies only templates it has onboarded, so multi-record one-click is unit-proven only until a provider serves one. Multi-record and email records has the honest per-path table.

Where this leaves us

ProviderServes both templatesReal apply proven
Glauca HexDNSsince 2026-07-29yes — TXT 2026-08-06, CNAME 2026-08-12
Domain Chiefsince 2026-07-28no
Cloudflaresince 2026-09-02no — and the sig-last order has never run live
GoDaddyno
IONOSno
1&1no

The state page is this table measured nightly, with template versions; the provider detector runs the same probe against whatever provider serves a domain you type in. On every provider in the "no" rows, and on any provider whose apply fails, the hosted flow falls back to guided manual records verified the same way — which is the part of the product that has to be right for Domain Connect to be worth adding at all.

Sources, all dated: the Domain Connect specification (v2.3 rev67) and draft-ietf-dconn-domainconnect-02; Cloudflare's Domain Connect reference (page dated 2026-04-16, re-read 2026-08-12); Domain-Connect/Templates#1436 and #1570; AS207960/hexdns#9; and DoDomain's own proof ledger, kept in the repository with the run evidence for 2026-08-06 and 2026-08-12.

On this page