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:
- Discovery — PSL registrable-zone resolution →
_domainconnectTXT lookup → provider/settingsfetch (SSRF-guarded). - Synchronous flow —
apply_url/2builds the "apply this template" URL to redirect the owner to. - Asynchronous (OAuth) flow —
async_consent_url/2→async_token/2→async_apply/3, plusasync_refresh/2, for applying templates programmatically. - Signed templates — pass
:private_key(RSA PEM) +:key_idto sign the apply request (RSA-SHA256) on either flow.
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
- Discovery. Compute the registrable zone via the Public Suffix List
(so
rent.theirplace.co.uk→ zonetheirplace.co.uk, hostrent), queryTXT _domainconnect.<zone>for the provider's API host, SSRF-check that host, thenGET https://<api-host>/v2/<zone>/settingsfor the provider's URLs. - 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
- Registrable zone only. Discovery uses the PSL registrable domain (matching the reference library). A record published on a delegated sub-zone isn't found.
- No template-support probe.
apply_url/2builds the URL; it doesn't call the provider API to confirm the template is supported. An unsupported template surfaces only at the provider's consent screen. - ASCII / punycode domains. Pass already-encoded (punycode) domains; IDNA conversion of Unicode domains isn't performed.
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