# Domain Connect field notes: every bug we hit (https://dodomain.io/docs/guides/domain-connect-field-notes)

What it took to ship signed, one-click DNS applies over Domain Connect: template registry, spec gaps, provider bugs, and why a probe's 200 proves nothing.

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](https://datatracker.ietf.org/doc/draft-ietf-dconn-domainconnect/), a public [template registry](https://github.com/Domain-Connect/Templates), and a [list of providers](https://www.domainconnect.org/dns-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 Glauca HexDNS, for the TXT template on 2026-08-06 and for the CNAME template on 2026-08-12. On Cloudflare both templates were applied and verified for real on 2026-09-25, and Cloudflare switched all five DoDomain templates to live mode for every customer the same day, and on 2026-09-26 the three multi-record templates were applied and verified there too. On Glauca HexDNS the CNAME + TXT template and every email-authentication group were applied and verified for real on 2026-10-22, and the apex template on 2026-10-23, so all five are proven there as on Cloudflare. On Vercel the CNAME template was applied and verified for real on 2026-10-20; its TXT template is served, apply not yet proven. Domain Chief, NameSilo, Simply.com and Danego DNS serve all five templates today, all with the apply not yet proven; three large ones do not serve them at all; and [the nightly-measured state of that](https://dodomain.io/state-of-domain-connect) 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 [#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; NameSilo and Vercel, found already serving them by a hand probe on 2026-10-15; Simply.com, found the same way on 2026-10-19; Danego DNS, found by a sweep of the registry's discovery records on 2026-10-27; 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 [#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 [#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 [#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](https://developers.cloudflare.com/dns/reference/domain-connect/) ("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 was only unit-tested until 2026-09-25, when the first real Cloudflare applies accepted it (section 11).

## 5. A provider's login wall proves nothing [#5-a-providers-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 [#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 [#7-then-the-providers-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](https://github.com/AS207960/hexdns/issues/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 [#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](https://dodomain.io/tools/dns-lookup) shows the authoritative answer and public resolvers side by side.

## 9. Applying over an existing record [#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 [#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, then proven [#11-cloudflare-served-then-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 `404`s, so it is not a blanket-`200` endpoint. It is also the provider that needs the `key`-then-`sig` order from section 4, which had never run live. So Cloudflare was **served, not proven**: no apply had happened on a Cloudflare-managed zone, and DoDomain did not describe itself as one-click on Cloudflare until a real apply landed. (Cloudflare-hosted zones had one-click anyway, through DoDomain's own OAuth path, which does not involve Domain Connect at all.)

The `200` turned out to be a preview: Cloudflare onboards a new service's templates restricted to one Cloudflare account, and switches them to live only after the service reports a working apply. The restriction is checked against the account that holds the **zone** being written, so our first attempts on a zone in a second account of ours hit a generic dashboard 404 rather than a permission error. Once that account was added, on 2026-09-25 both templates applied for real on a Cloudflare-hosted zone we control: Cloudflare's consent page accepted the `key`-then-`sig` URL, the TXT and the CNAME answered on public resolvers within 30 seconds, and both sessions verified. Cloudflare switched all five DoDomain templates to live mode for every customer the same day.

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 [#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](https://dodomain.io/state-of-domain-connect), 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 [#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 byte-locked to the in-code registry by a test, were merged into the registry on 2026-09-10 (Domain-Connect/Templates#1781), and are served by Domain Chief and Glauca HexDNS since 2026-09-11, by Cloudflare, to every customer, since 2026-09-25, and by NameSilo (found 2026-10-15), Simply.com (found 2026-10-19) and Danego DNS (found 2026-10-27). Vercel does not serve them yet. On 2026-09-26 all three were applied for real on Cloudflare-hosted zones we control and verified on the authoritative nameservers: the CNAME + TXT pair, the apex `A` + TXT (Cloudflare listed the zone's existing apex `A` for removal and made us type the zone name to confirm), and every `email-authentication` group: DKIM as `CNAME` and as `TXT`, DMARC, and the SPF merge, where Cloudflare folded our `include:` into the zone's existing SPF record and kept its other terms. On 2026-10-22 the CNAME + TXT pair and every `email-authentication` group were applied and verified the same way on Glauca HexDNS, where the apply replaced the zone's default no-mail SPF and DMARC records rather than duplicating them, and on 2026-10-23 the apex `A` + TXT followed on a second Glauca zone delegated from a Cloudflare-hosted parent (Glauca's consent page listed exactly the two new records, with nothing to delete). On Domain Chief, NameSilo, Simply.com and Danego DNS they are served, apply not yet proven. [Multi-record and email records](https://dodomain.io/docs/multi-record-and-email) has the honest per-path table.

## Where this leaves us [#where-this-leaves-us]

| Provider      | Templates served                                                                       | Real apply proven                                                                                                                         |
| ------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Cloudflare    | all five (the two originals since 2026-09-02, the three multi-record since 2026-09-18) | **yes**, all five: TXT and CNAME 2026-09-25; the three multi-record templates 2026-09-26                                                  |
| Glauca HexDNS | all five (the two originals since 2026-07-29, the three multi-record since 2026-09-11) | **yes**, all five: TXT 2026-08-06, CNAME 2026-08-12, CNAME + TXT and every email-authentication group 2026-10-22; apex A + TXT 2026-10-23 |
| Domain Chief  | all five (the two originals since 2026-07-28, the three multi-record since 2026-09-11) | apply not yet proven                                                                                                                      |
| NameSilo      | all five (found 2026-10-15)                                                            | apply not yet proven                                                                                                                      |
| Simply.com    | all five (found 2026-10-19)                                                            | apply not yet proven                                                                                                                      |
| Danego DNS    | all five (found 2026-10-27)                                                            | apply not yet proven                                                                                                                      |
| Vercel        | the two originals only (found 2026-10-15)                                              | **yes** for CNAME 2026-10-20; TXT: apply not yet proven                                                                                   |
| GoDaddy       | none                                                                                   | n/a                                                                                                                                       |
| IONOS         | none                                                                                   | n/a                                                                                                                                       |
| 1&1           | none                                                                                   | n/a                                                                                                                                       |

The [state page](https://dodomain.io/state-of-domain-connect) is this table measured nightly, with template versions; the [provider detector](https://dodomain.io/tools/provider-detector) runs the same probe against whatever provider serves a domain you type in. On every provider in the "none" rows, for any template a provider does not serve, 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, 2026-08-12, 2026-10-22 and 2026-10-23._
