DomainConnect

An Elixir client for the Domain Connect protocol — the open standard (now IETF-track) for one-click DNS setup so a non-technical domain owner can point a custom domain at your app without ever touching a CNAME record.

When someone connects rent.theirplace.com to your SaaS, instead of "create a CNAME with host rent and value portal.yourapp.com, then wait for DNS," they click a button, consent at their own DNS provider, and the records are applied. ~20 providers implement it, including GoDaddy, IONOS, Cloudflare, Squarespace Domains, WordPress.com, and Plesk (≈35% of the .com zone).

Status

Covers the Service-Provider side of both flows:

Every request this library makes server-side (settings fetch, token exchange, apply, refresh) is SSRF-guarded: the DNS-influenced host is validated as a public hostname (no IP literals, no private/loopback/link-local/ULA addresses), redirects are disabled, and timeouts are tight.

Install

def deps do
[{:domain_connect, "~> 0.5"}]
end

Usage

# 1. Does this domain's DNS provider support Domain Connect, and how?
{:ok, config} = DomainConnect.discover("rent.theirplace.com")
#=> %DomainConnect.Config{domain: "theirplace.com", host: "rent",
# provider_id: "GoDaddy", url_sync_ux: "https://dcc.godaddy.com/manage", ...}
# 2. Build the URL to send the owner to. They click "apply" at their provider,
# the records land, and the custom domain goes live.
{:ok, url} =
DomainConnect.apply_url(config,
provider_id: "yourapp.com", # YOUR template's providerId
service_id: "custom-domain", # YOUR template's serviceId
params: %{"target" => "portal.yourapp.com"}
)
# redirect_to(conn, external: url)

provider_id / service_id identify your Domain Connect template — the record set you register with each DNS provider — not the DNS provider itself.

Just checking support

DomainConnect.supported?("rent.theirplace.com") #=> true | false

Asynchronous (OAuth) flow

For applying templates programmatically instead of a one-shot redirect:

{:ok, config} = DomainConnect.discover("rent.theirplace.com")
# 1. Send the owner to consent.
{:ok, consent_url} =
DomainConnect.async_consent_url(config,
provider_id: "yourapp.com",
service_ids: "custom-domain", # one id or a list -> OAuth scope
redirect_uri: "https://yourapp.com/dc/callback",
state: "opaque"
)
# 2. On the callback, exchange the code for a token.
{:ok, token} =
DomainConnect.async_token(config,
code: code, client_id: "yourapp.com", client_secret: secret,
redirect_uri: "https://yourapp.com/dc/callback"
)
# 3. Apply the template (idempotent; {:error, :conflict} unless force: true).
:ok =
DomainConnect.async_apply(config, token,
provider_id: "yourapp.com", service_id: "custom-domain",
params: %{"target" => "portal.yourapp.com"}
)
# Later: DomainConnect.async_refresh(config, refresh_token: token.refresh_token, ...)

How it works

  1. Discovery. Compute the registrable zone via the Public Suffix List (so rent.theirplace.co.uk → zone theirplace.co.uk, host rent), query TXT _domainconnect.<zone> for the provider's API host, SSRF-check that host, then GET https://<api-host>/v2/<zone>/settings for the provider's URLs.
  2. Apply. Build <url_sync_ux>/v2/domainTemplates/providers/<provider_id>/services/<service_id>/apply?domain=…&host=…&<vars> and redirect the owner there.

The DNS resolver, address resolver (SSRF guard), and HTTP client are injectable for testing (:resolver, :address_resolver, :req_options on discover/2).

Limitations

The template-registration caveat

The library builds correct requests for any provider. To actually light up a given provider in production, you register your template (the records your service needs) with that provider — GoDaddy, IONOS, etc. each have a template onboarding step. That's operational, per-provider, and one-time; it isn't a prerequisite for using this library or for the providers you've onboarded.

License

MIT