Azure SWA Custom Domains
Purpose
Take a provisioned Azure Static Web App from its *.azurestaticapps.net hostname to a live, HTTPS-served custom domain — apex, www, and any subdomains — with the right DNS records, validation method, canonical redirect, and TLS binding, plus a diagnosis path for the platform's stalls and wedges. Split from static-website-hosting, which provisions and deploys the site itself.
When to use
After the SWA exists and deploys (see static-website-hosting), whenever you:
- Add
www, the apex, or another subdomain (e.g.go.<domain>) to a Static Web App - Move a domain from previous hosting onto SWA (cutover)
- See
Readydomains that still fail TLS, "CNAME Record is invalid", or a hostname stuck inFailed/Deleting - Need apex → www canonicalisation or want to verify DNS/HTTPS/redirects after a change
Inputs expected
Partial inputs are fine — ask only for what is missing.
- Domain name and which host is primary (default:
wwwprimary, apex redirects) - DNS provider and whether it supports ALIAS/ANAME/CNAME-flattening at the apex
- SWA name, resource group, subscription, and its default hostname
- Any existing records on the domain (parking A records, old hosting, mail records) and their TTLs
Guiding principles
- Use
wwwas the primary domain, and canonicalise apex → www by makingwwwthe app's default domain. SWA route rules cannot match on host, so the redirect is done by ARMPUTon thewwwcustomDomains resource withproperties.isDefault: true(PATCHreturns Method Not Allowed; includevalidationMethod: cname-delegationso aReadydomain is adopted in place). The apex then 301s to www with path and query preserved. Keep the apex on its ALIAS/CNAME-flattening record to the default hostname; do not re-point it at thestableInboundIPjust to obtain a redirect. See Default-domain ARM PUT inreference.md. - Validate the apex with
dns-txt-tokenvia the REST API (az rest --method put). The standardaz staticwebapp hostname setfails for apex domains when DNS has not yet propagated. - A subdomain (www or
go.<domain>) uses CNAME validation — no TXT. Add<sub>CNAME →<defaultHostname>.azurestaticapps.net, then bind it (az staticwebapp hostname setor BicepcustomDomainsbehind a bool param that staysfalseuntil the CNAME exists) and Azure auto-issues the managed TLS cert. Hostnamestatus: Readymeans validated and certificate issued. - "CNAME Record is invalid" is usually timing, not a typo. Registering a
wwwhostname fails until Azure's own resolvers observe the new record, and a just-changed record can be masked for hours by the old record's TTL (a 4-hour parking A record, for instance). Script a retry every few minutes rather than treating the first failure as terminal. ReadywithsslState: nulland a TLS internal-error alert is a stalled binding. The domain reports Ready but the edge answers with no certificate (browsers showERR_SSL_PROTOCOL_ERROR). Fix by deleting and re-adding the hostname to force fresh certificate issuance; awwwre-add validates instantly over an existing CNAME. Allow up to 15 minutes propagation after the re-add. This fix applies only when status is Ready with null sslState — never after a Failed add.- Static apex A records go stale. Azure can change the underlying IP; document the current IP with a note to re-verify after any Azure infrastructure event, and prefer ALIAS/ANAME/CNAME-flattening to the default hostname where the DNS provider supports it.
- After a cutover, clients keep resolving the old IP for up to the old record's TTL. A parking host with no certificate presents as
ERR_SSL_PROTOCOL_ERROReven though the new hosting is healthy. Diagnose with freshcurlconnections from another resolver before touching the server; fix clients withipconfig /flushdnsand a browser restart. - Custom-domain operations can wedge permanently — recognise it early and escape sideways.
hostname setgoesRetrievingValidationToken→Failed("unknown error"), the follow-up delete sticks inDeletingindefinitely, and CLI, direct ARMDELETEand Portal deletes all accept-then-fail about 10 minutes later with no customer-side force-delete (a Basic support plan cannot even file a ticket; Resource Health does not supportstaticSites, so harvest correlation IDs fromaz monitor activity-log list --resource-group <rg> --offset 24hfiltered oncustomDomains). The stuck claim follows the hostname, not the resource: rebuilding the SWA under a new name re-wedges,az staticwebapp deleteon such a resource silently no-ops (showstill succeeds and a same-name Bicep redeploy reuses it). Treat "Operation returned an invalid status 'OK'" fromhostname set/deleteas a CLI poller quirk — verify withhostname list, not the exit code — and escape a wedged name by serving it from a different Azure service entirely (e.g. an Azure Container App; seebranded-link-qr-service). - Never let automation register or delete a hostname inside a fragile platform loop. The community workaround for a wedged domain (delete the CNAME at the registrar, wait 30–60 minutes, re-issue the delete) cleared the stuck record once — but an automated watcher immediately re-registered the hostname into empty DNS and re-wedged it, and the second attempt never cleared. After changing or deleting a DNS record, budget a full old-TTL soak (60 minutes seen, negative caching included) before a single manual registration attempt; watchers observe and report, a human performs the mutating step.
- SWA Free tier appears to cap custom domains at 2 per app (apex + www consumed both); verify the current limit before planning more subdomains, and plan additional subdomains as separate SWA resources rather than extra domains on the same app.
- Write a validation script that checks DNS, HTTPS, and redirect behaviour on every hostname, and run it after every deployment or DNS change.
Process
- Inventory DNS — current records at apex/www/subdomains, their TTLs, and any parking or old-hosting records that will mask a change.
- Publish records —
www(and other subdomains): CNAME → SWA default hostname; apex: ALIAS/ANAME/flattened CNAME to the default hostname, or an A record to the SWA IP (nslookup <defaultHostname>) if that is all the provider offers. - Confirm records from a public resolver (
nslookup … 8.8.8.8/ DoH) — then still expect Azure's resolvers to lag by the old TTL. - Bind subdomains —
az staticwebapp hostname set(retry every few minutes on "CNAME Record is invalid") or Bicep withdeployCustomDomain=true. - Bind the apex —
az rest --method putwithvalidationMethod: dns-txt-token, add the returned TXT at@, complete validation. See Apex TXT validation inreference.md. - Wait for
Readyand prove TLS —az staticwebapp hostname list, thencurl -sI https://<host>andopenssl s_client -servername <host>showing the right CN. Ready +sslState: null+ TLS alert → delete and re-add the hostname. - Canonicalise — ARM PUT
isDefault: trueonwww; confirmhttp://<apex>/x?y=1→ 301 →https://www.<apex>/x?y=1. - Run the validation script and record hostnames, IP, records, and TTLs in the operator notes.
- If a hostname wedges (Failed / stuck Deleting) — collect activity-log correlation IDs, stop retrying against SWA, and serve that hostname from another service. If you try the delete-CNAME-and-wait workaround, disable any watcher/automation first, soak a full old TTL, then make one manual attempt.
Output format
- DNS record table — hostname, type, value, TTL, and any old record to retire
- Binding commands — subdomain
hostname set, apexaz restTXT validation, default-domain PUT - TLS/redirect verification —
hostname liststatus,curl/openssloutput, apex → www redirect check - Validation script — DNS, HTTPS, redirect checks for every hostname
- Operator notes — current IP, TTLs, retry/cutover expectations, and the wedge escape plan
Quality checklist
-
wwwis the SWA default domain (ARM PUTisDefault: true); apex 301s to www with path + query preserved - Apex validated via
dns-txt-token; subdomains via CNAME — no TXT for subdomains - Every hostname shows
Readyand serves a certificate (openssl s_clientCN correct); nosslState: nullstalls - "CNAME Record is invalid" handled by scripted retry, not by editing DNS again
- Apex on ALIAS/ANAME/flattening where possible; any A record IP documented with a re-verify note
- Old-record TTLs noted; cutover verified from a fresh resolver before blaming the server
- Validation script runs clean on all hostnames
- Wedged-hostname signals (Failed add, stuck Deleting, "invalid status 'OK'") recognised — verified with
hostname list, escape route named - No automation registers/deletes hostnames during DNS soaks; Free-tier 2-domain cap checked before planning subdomains
Avoid
- Managing the apex domain in Bicep — its
dns-txt-tokenvalidation is a separate lifecycle from provisioning - Re-pointing the apex at the
stableInboundIPto get a redirect — setisDefaultonwwwinstead - Treating the first "CNAME Record is invalid" as terminal — Azure's resolvers lag; retry
- Trusting
Readyalone — checksslStateand an actual TLS handshake - Applying the delete/re-add TLS fix after a Failed add — it only works on Ready + null sslState
- Trusting the exit code of
hostname set/delete— "invalid status 'OK'" is a poller quirk; readhostname list - Rebuilding a wedged SWA under a new name expecting the hostname to free up — the claim follows the hostname
- Debugging
ERR_SSL_PROTOCOL_ERRORon the server when clients are still resolving the old IP — flush client DNS first - Leaving a watcher or CI job free to re-register a hostname while you are clearing a wedge — it re-wedges it into empty DNS
- Planning a third custom domain on a Free-tier app — check the cap; use a separate SWA per extra subdomain
Example usage
"My Static Web App is deployed on its
azurestaticapps.nethostname. Moveexample.com.auandwww.example.com.auonto it — DNS is at VentraIP, there's a 4-hour parking A record on the apex right now — and make sure the apex redirects to www over HTTPS. Yesterdaywwwshowed Ready but the browser gave a TLS error."
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.