Custom domains
Self-hosting
Turn on customers' custom link domains on a self-hosted deployment with Cloudflare for SaaS, and how the Worker validates, serves, re-checks and deletes them.
Custom link domains (go.yourbrand.com, one per account) use Cloudflare for SaaS custom hostnames on the deployment’s zone. The feature is off until the three settings below are present; the Settings card then says it is not set up and the admin routes answer 409 not_configured. What account holders see is in Links: custom domains.
Turn it on
Section titled “Turn it on”-
In the Cloudflare dashboard, open the zone (
dectog.combelow) and enable SSL/TLS > Custom Hostnames (Cloudflare for SaaS). -
Add a proxied DNS record for the target customers point at, for example
domains.dectog.comasAAAA 100::, and set it as the fallback origin on the same page. -
Create an API token with the SSL and Certificates: Edit permission on the zone.
-
Set the secrets and the variable:
npx wrangler secret put CF_API_TOKEN --env productionnpx wrangler secret put CF_ZONE_ID --env productionand
"CUSTOM_DOMAIN_TARGET": "domains.dectog.com"underenv.production.varsinapps/worker/wrangler.jsonc. -
Route the customers’ hostnames to the Worker: uncomment the
*/*route next todectog.com/*and*.dectog.com/*inwrangler.jsonc. The existing routes already cover every host on the zone, so the new one adds only custom hostnames. -
Deploy. The
custom_domainstable is already part of the schema, so there is no migration to apply.
On a deployment made with npm run setup, steps 4 and 5 are one option: put the two secrets with --config wrangler.self-host.jsonc in place of --env production, and pass --custom-domain-target domains.example.com to setup, which adds the variable and the */* route (setup rewrites its config on every run, so pass it every time). The Terraform module with custom_domains_origin = true does step 2.
Cloudflare includes a number of custom hostnames with each plan and charges per hostname beyond it; see Cloudflare for SaaS pricing.
How the Worker handles a domain
Section titled “How the Worker handles a domain”| Step | What happens |
|---|---|
| Add | POST /zones/<zone>/custom_hostnames with ssl: {method: "txt", type: "dv"}. The answer’s ownership_verification and ssl.validation_records become the TXT records the card shows, plus the CNAME to CUSTOM_DOMAIN_TARGET. A hostname another Cloudflare zone already has is refused (409 hostname_taken). |
| Check | GET the custom hostname. Its status and ssl.status map to the six states: pending_validation, issuing_certificate, ready (both active), active, moved, failed (blocked, deleted, or a certificate step timed out). |
| Ready to active | A DNS-over-HTTPS CNAME lookup (cloudflare-dns.com) that finds CUSTOM_DOMAIN_TARGET; or a request that reaches the Worker on the host while public DNS sends it to Cloudflare (the CNAME, or A/AAAA records all inside Cloudflare’s ranges from /client/v4/ips, for proxied records), looked up at most once a minute per host. A request alone is not enough: a pre-validated hostname is on Cloudflare’s edge before the customer moves DNS, so anyone can reach it. Then users.custom_domain is set, and every link URL is built on it. |
| Re-check | An active domain trusts Cloudflare’s own status: moved means it no longer points at the fallback origin. The domain then stops being canonical, and is deleted after 7 days unless it comes back. |
| Remove | Suspends: users.custom_domain is cleared at once, the host is still served, and the hostname is deleted 7 days later. A domain that never served is deleted at once. |
The hourly cron (sweepCustomDomains) checks pending domains not checked for ten minutes, re-checks active ones once a day and deletes the ones past their grace, 50 of each per run. The dashboard card checks every minute while it is open on a pending domain.
A request on any host that is not the root domain, one of its subdomains, APP_HOST, the PUBLIC_BASE_URL or WEB_URL host, localhost, an IP or *.workers.dev is looked up in custom_domains (states ready, active and moved, and suspended ones within their grace). The answer is cached per isolate for a minute, misses included, so a tap on the default hosts costs no extra read and a tap on a custom host at most one a minute.