Custom Domain
Pointing a domain at a site is four minutes of real work wrapped in up to twenty-four hours of waiting, and the waiting is where people break things. The DNS is usually right long before the host admits it. Someone who doesn't know that will "fix" correct records three times, enable a proxy setting that stops the host verifying the domain at all, and end up further from working than when they started.
So the job here is mostly about order and patience, plus one category of failure that is genuinely nasty: settings elsewhere in the stack that were keyed to the old address and now silently refuse to work. The site loads fine. Login doesn't. Nothing logs an error that says why.
The core mindset
The host tells you the records. Never guess them. Every platform has its own values and they change. Add the domain in the host's dashboard first and copy what it gives you.
Verify DNS yourself before you believe any dashboard. dig against two
public resolvers is ground truth. A host saying "not detected" while dig
returns the right answer means wait, not change something.
Assume something else in the stack knows the old URL. Auth providers, captcha keys, OAuth apps, CORS allowlists, webhooks, email templates, CSP headers. Each keeps its own list of "domains I trust". Phase 3 is the one people skip and it is the one that produces "the site works but I can't log in".
Don't move the canonical URL until HTTPS actually serves. Telling search
engines the real address is https://newdomain.com while that host has no
certificate is worse than leaving it pointed at the old one for another hour.
Phase 0 — Find out what is actually moving
Before touching anything, establish these. Most can be read from the repo; ask only for what you genuinely can't determine.
- Which host serves the site? Look for
firebase.json,wrangler.toml,vercel.json,netlify.toml, agh-pagesbranch or Pages workflow. This decides everything downstream — read the matching file inreferences/. - Where is the domain registered, and who runs DNS? Registrar and DNS host
can differ. What matters is which nameservers the domain actually uses:
dig NS <domain> +short. - Is there an apex/www preference?
example.comorwww.example.comas the real address. Pick one to serve and redirect the other; serving both is a duplicate-content problem. - Does the app authenticate users? If yes, Phase 3 is mandatory, not optional.
- Is the old URL staying alive? Usually yes and usually free. Say so — people worry that existing links will die.
- Is anything already broken? If they're mid-attempt, run
scripts/check-domain.shfirst and work from real state rather than from their description of it.
Phase 1 — Add the domain at the host, and copy the records it gives you
Do this before touching DNS. The host generates the exact records — often including a one-time verification TXT unique to that site — and there is no way to know them in advance.
Read the relevant file in references/ for the host's exact flow. Typically
you'll get either an A record (apex, most Firebase/GitHub setups) or a
CNAME (subdomains, and most Vercel/Netlify/Cloudflare Pages setups), plus
sometimes a TXT proving ownership.
Record what you were given verbatim before moving on. If you're setting up both apex and www, note that they are usually two separate domain entries with two different records.
Phase 2 — Create the DNS records at the registrar
Find out who actually runs DNS first — registrar and DNS host are often
different, and editing records at the wrong one changes nothing while appearing
to work: dig NS <domain> +short.
Then read the matching reference: references/cloudflare-dns.md if the answer
is Cloudflare (its default proxy setting stops the host from ever verifying the
domain, while the site still appears to work — a genuinely confusing failure),
otherwise references/registrars.md for Namecheap, GoDaddy, Squarespace,
Route 53, and the field-name differences between them.
General rules that hold everywhere:
- Name/host field:
@(or blank, or the bare domain) means the apex.wwwmeanswww.example.com. Never type the full domain into a field that is already scoped to the zone — that producesexample.com.example.com. - Delete conflicting records first. An existing A, AAAA, or CNAME on the same name will fight the new one. Parked-domain placeholders are the usual culprit. An AAAA record left behind is a classic cause of "works for me, broken for them" — IPv6 clients get the stale answer.
- TTL: low (300s / "automatic") while you're setting up.
- Apex CNAME: not legal in plain DNS. Many providers offer an equivalent under another name — ALIAS (Namecheap BasicDNS, DNSimple), ANAME (easyDNS), CNAME flattening (Cloudflare), or Alias records (Route 53, for AWS targets). If your host wants a CNAME at the apex, look for one of those before assuming you have to move DNS.
Phase 3 — Fix everything that trusted the old address
This is the phase that separates a working move from a mysterious one. Each item below fails silently: the page loads, and one feature is dead with no error that names the cause. Walk the list and check each against the app.
| What breaks | Where it lives | Symptom if missed |
|---|---|---|
| The list of origins auth will accept | Firebase: Auth → Settings → Authorized domains. Supabase: Authentication → URL Configuration → Site URL + Redirect URLs. Auth0/Clerk: Allowed Callback/Logout URLs. | Site loads, sign-in popup opens and immediately closes, or the callback lands on an error page. Nothing else breaks. |
| OAuth redirect URIs in the Google/GitHub/Apple console | Only if your app receives the callback directly. If an auth provider brokers it (Supabase, Auth0, Clerk), the URI points at their domain and does not change when your site's domain does. | redirect_uri_mismatch. Check before editing — changing a brokered URI breaks a working setup. |
| Captcha / bot-protection domain list | reCAPTCHA admin console, Turnstile, App Check | Tokens fail validation. With enforcement on, every gated action fails at once. |
| CORS allowlist on any API the site calls | your API, or the provider's dashboard | Fetches blocked from the new origin only. |
CSP connect-src / frame-src |
headers or meta tag | Console CSP violations; embedded widgets and auth popups die. |
| Webhook + callback URLs | Stripe, payment, third-party dashboards | Events stop arriving; often unnoticed for days. |
| Email link domains | often the same field as the auth origin (Supabase's Site URL renders both) — don't hunt for a second knob that doesn't exist | Password-reset and magic links still point at the old host. |
| Analytics property / site config | GA, Plausible, etc. | Traffic silently stops recording. |
Treat this as a list of failure modes, not a list of settings: every provider names these differently and some collapse several into one field. Find the provider's actual vocabulary rather than searching for these exact words — searching Firebase's terms in a Supabase dashboard finds nothing and wastes real time.
For each one you change, confirm the setting actually persisted — several of these consoles fail to save without saying so. Reload the page and re-read the value.
Phase 4 — Verify DNS before you wait on anything
Run the bundled checker:
bash scripts/check-domain.sh example.com
It queries multiple public resolvers, follows the HTTP→HTTPS redirect, reports
the certificate's issuer and names, and checks www. Use it rather than one
dig against the local resolver — local caches and the ISP resolver are the
least representative view of what the rest of the world sees.
Read the result honestly. If the records are correct from two independent resolvers, DNS is done. Any remaining "not detected" is the host's own cached view, and the fix is to wait. Do not start editing correct records.
Phase 5 — Move the site's idea of its own URL, but don't ship it yet
Most sites hard-code their address somewhere: a SITE_URL constant, canonical
tags, sitemap.xml, robots.txt, Open Graph URLs, JSON-LD, and any absolute
links. Find them:
grep -rIn "old-host\.\(web\.app\|pages\.dev\|vercel\.app\|netlify\.app\|github\.io\)" --exclude-dir=node_modules .
Also check the host's environment variables, not just the repo. Vercel,
Netlify, and Cloudflare Pages all commonly hold the canonical URL as a
dashboard env var (NEXT_PUBLIC_SITE_URL, URL, SITE_URL) that no amount of
grepping the tree will surface — a fresh clone doesn't even contain the file.
Two consequences worth stating plainly: build-time-inlined vars (anything
NEXT_PUBLIC_*, VITE_*) do nothing until a redeploy, and that redeploy is
the same one Phase 6 tells you to hold. So change the value now, ship it later.
Framework-specific places the URL hides: metadataBase in a Next.js root
layout, app/sitemap.ts / app/robots.ts, astro.config site:, and any
sitemap plugin config.
Change them to the new domain, keeping the old value as a named constant if anything still references it. Then stop. Build it, verify the output, and hold the deploy until Phase 6 confirms HTTPS is live. Canonical tags pointing at a host that serves a certificate error is the one genuinely harmful state in this whole process.
Phase 6 — Wait for the certificate, and don't poison the wait
Certificate issuance starts only after the host has verified DNS, and hosts poll on their own schedule. Typical is minutes; the documented ceiling is 24 hours and it does sometimes take hours.
What's normal, and worth telling the user so they don't panic:
http://redirecting tohttps://whilehttps://still fails. It means DNS is reaching the host and only the certificate is outstanding. This is the expected intermediate state, not a problem.- The host's dashboard lagging behind reality by hours.
- Apex and www issuing at different times when added separately.
On .dev, .app, and .page, the redirect reassurance above does not
apply. Those TLDs are HSTS-preloaded as a whole, so browsers rewrite http://
to https:// before sending anything — during the wait the user gets a hard TLS
error page with no click-through, which looks far more broken than it is.
curl ignores preloading, so scripts/check-domain.sh still tells the truth;
lean on it rather than on the browser, and warn the user in advance.
The one thing that will genuinely prevent issuance forever is a proxy or
redirect intercepting the validation request. On Cloudflare that is the
orange-cloud proxy — see references/cloudflare-dns.md.
Poll with the checker script rather than clicking Verify repeatedly.
Phase 7 — Add the other half of the pair
Once the primary domain serves, add www (or the apex, if you led with www).
Prefer a redirect to the canonical host over serving the site twice: two
hosts serving identical content splits SEO signal, and a redirect consolidates
it. Several hosts offer this natively when you add the second domain.
It needs its own DNS record and gets its own certificate, so expect it to trail the first one.
Phase 8 — Deploy and confirm
Now ship the Phase 5 changes, then verify against the live site rather than declaring success:
- The domain serves over HTTPS with a valid certificate.
http://and the www/apex counterpart both redirect to the canonical host.- Canonical tag,
sitemap.xml, androbots.txtshow the new domain. - Sign in. Actually complete a login if the app has one — this is the check that catches a missed Phase 3 item, and nothing else will.
- The old platform URL still loads, so existing links survive.
Report what's live, what's still propagating, and anything you changed in another console that they should know about.
Traps
Enabling a CDN proxy on the DNS record. Blocks certificate validation. Cloudflare's is on by default and it nags you to turn it on. See the reference.
Trusting a single dig. Local and ISP resolvers cache aggressively. Two
public resolvers or it didn't happen.
Leaving a stale AAAA record. IPv6 clients silently get the old answer while it works fine for you.
Re-editing correct DNS because a dashboard says "not detected". The most common way people turn a 20-minute wait into a broken afternoon.
Assuming an auth provider's own domain moves with your site. Sign-in popups and callbacks often stay on the provider's host — which is why the fix is usually adding your new domain to their allowlist, not rewriting URLs.
Shipping canonical URLs before the certificate exists.
What needs the user, not you
You can do nearly all of this. These need them:
- Buying the domain and any registrar-level payment.
- Changing nameservers at the registrar, if DNS is moving.
- Consoles you have no session for — hand them the exact values to paste and the exact page, rather than a description of where to look.
- Deciding apex vs www as the canonical address, if they have a preference.
Reference files
references/firebase-hosting.md— Firebase, incl. Auth authorized domains and App Check, the two things that break login after a move.references/cloudflare-dns.md— the proxy trap, apex flattening, and a reliable record-creation path when the dashboard UI resists automation.references/vercel.md— Vercel, incl. the per-project CNAME and the build-time env var that holds the canonical URL.references/other-hosts.md— Cloudflare Pages, GitHub Pages, Netlify.references/registrars.md— Namecheap, GoDaddy, Squarespace, Route 53, Porkbun, and how to tell who actually runs DNS.scripts/check-domain.sh— resolver-independent DNS, redirect, and certificate check. Run it instead of clicking Verify.