# DomainConnect
An Elixir client for the [Domain Connect](https://www.domainconnect.org) protocol —
the open standard (now [IETF-track](https://datatracker.ietf.org/doc/draft-ietf-dconn-domainconnect/))
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 → `_domainconnect` TXT lookup → provider `/settings` fetch (SSRF-guarded).
- **Synchronous flow** — `apply_url/2` builds the "apply this template" URL to redirect the owner to.
- **Asynchronous (OAuth) flow** — `async_consent_url/2` → `async_token/2` → `async_apply/3`, plus `async_refresh/2`, for applying templates programmatically.
- **Signed templates** — pass `:private_key` (RSA PEM) + `:key_id` to 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
```elixir
def deps do
[{:domain_connect, "~> 0.5"}]
end
```
## Usage
```elixir
# 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
```elixir
DomainConnect.supported?("rent.theirplace.com") #=> true | false
```
### Asynchronous (OAuth) flow
For applying templates programmatically instead of a one-shot redirect:
```elixir
{: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
- **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/2` builds 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