Glossary/Failure modes
Universal Links stopped working
Definition
Universal Links that previously worked and no longer do have almost always been broken by a change outside the app: a per-domain user preference, a signing or infrastructure change, or Apple's CDN refreshing its cached copy of a file that was already broken.
A regression is a different diagnostic problem from a setup that never worked, because the space of causes is not "everything that can be wrong" but "everything that changed". That is a much smaller list, and the most common entry on it is not a change you made — it is a preference one user set on one device by tapping something they did not understand. Work out first whether this is happening to everyone or to one person, because that single fact splits the list in half.
One device, or everyone?
Ask this before anything else. It is the cheapest question available and it eliminates most of the page.
| Scope | Almost always | First thing to try |
|---|---|---|
| One device, or a handful of complaints | The user set a per-domain preference to open in Safari | Long-press the link → Open in “App” |
| Everyone, starting at a known moment | An infrastructure or signing change | curl -sSIL the association file and diff against a known-good deploy |
| Everyone, gradually over a day | Apple's CDN refreshed and picked up a break | Check the CDN copy against your origin |
| Only new installs | The association is failing; existing installs hold a cached verdict | Delete and reinstall on a test device |
| Only existing installs | An app update dropped the entitlement | codesign -d --entitlements on the shipped build |
The user preference nobody knows exists
When a Universal Link opens your app, iOS shows a small breadcrumb in the top-right of the status bar with the domain name on it. Tapping that breadcrumb returns the user to Safari — and it also records a per-domain preference that says: from now on, open this domain in Safari rather than the app. The user is never told this. From their point of view they tapped a back button once, and Universal Links quietly stopped working for your entire domain, permanently.
The same preference is set by long-pressing a link and choosing Open instead of Open in “App”. Both are things users do by accident, and neither is discoverable in Settings — there is no toggle anywhere in iOS that lists or resets these preferences.
How a user undoes it
Long-press the link and choose Open in “App” from the context menu. That flips the preference back, and links behave normally again afterwards. This is the entire fix, it takes three seconds, and it is worth putting verbatim into your support macros — a meaningful share of "deep links are broken" tickets are this and nothing else.
Deleting and reinstalling the app also clears it, which is why support's default advice appears to work and produces no useful information about the real cause. If a reinstall fixes it for one user and the problem does not recur across your install base, this was it.
The changes that break it for everyone
A redirect appeared in front of the association file. The most common site-wide regression by a wide margin, and it is almost never deliberate — an added locale prefix, a new trailing-slash normaliser, a www canonicalisation, or a CDN rule. Apple's fetcher does not follow redirects, so a 301 is a failure. The web team's change log is the place to look, not the app's.
The App ID Prefix or bundle identifier changed. Transferring an app between developer accounts, or being migrated onto a new Team ID, changes the App ID in ways that make the deployed AASA file stale while remaining perfectly valid JSON. Nothing errors — the file simply names an app that no longer exists.
An app update shipped without the entitlement. A regenerated provisioning profile that lost the Associated Domains capability, or a CI signing change, produces a build that cannot claim the domain. The tell is that the break correlates exactly with a release, and only affects users who took the update.
Apple's CDN finally refreshed. This one is genuinely counter-intuitive: you broke the file weeks ago, the CDN kept serving its cached good copy, and everything worked until the refresh. The change that caused the outage and the outage itself can be separated by weeks, which makes the deploy correlation misleading. Always compare the CDN copy with your origin rather than assuming they agree.
# Apple's cached copy — what devices are actually being given
curl -sS https://app-site-association.cdn-apple.com/a/v1/example.com \
| python3 -m json.tool > /tmp/cdn.json
# Your origin, no redirects followed, so a 301 shows up as a 301
curl -sS --max-redirs 0 \
https://example.com/.well-known/apple-app-site-association \
| python3 -m json.tool > /tmp/origin.json
diff /tmp/cdn.json /tmp/origin.json && echo "in sync"
# Headers only: one 200, application/json, nothing else
curl -sSI --max-redirs 0 \
https://example.com/.well-known/apple-app-site-associationThe TLS certificate expired or the chain changed. The fetch requires a valid, trusted certificate. An expired intermediate, or a certificate that browsers accept because they have a cached chain, will fail a fetch that has no such cache. Renewals that "worked fine, the site is up" are a recurring source of this.
`?mode=developer` was left in a release build. It bypasses the CDN and makes every device fetch your origin directly. It survives review, works in testing, and turns your origin into a dependency of every app launch — which fails the first time that host has a bad day.
Making the regression impossible to repeat
Six of the causes above are infrastructure, and infrastructure changes are made by people who have no reason to know that a JSON file in .well-known is load-bearing for the mobile app. The durable fix is not vigilance, it is a check that fails on the deploy that introduces the problem.
- Assert in CI that
curl -sSI --max-redirs 0against production returns exactly200withContent-Type: application/json. This catches the redirect regression, the auth-wall regression, and the SPA-catch-all regression on the deploy that causes them. - Assert the App ID in the deployed file matches the one in the app's entitlement, so a Team ID migration fails the build rather than the links.
- Add a certificate expiry alert on the host serving the file, separate from any alert on the marketing site.
- Add
?mode=developerto the list of strings your release pipeline greps for and refuses to ship. - Put the long-press fix in your support macros, so the per-device preference stops arriving as an engineering escalation.
deep link debugger
Point it at your domain and it compares Apple's cached copy against your live origin, follows and reports the redirect chain, checks the content type and certificate, and flags a stale App ID — which covers every site-wide cause on this page in one pass.
Open the deep link debugger →Frequently asked questions
- Why did my Universal Links suddenly start opening in Safari?
- If it affects one device, the user almost certainly set a per-domain preference by tapping the domain breadcrumb in the top-right of the status bar after the app opened, or by choosing Open rather than Open in App from a long-press menu. iOS records that choice permanently and never surfaces it in Settings. Long-pressing a link and choosing Open in App reverses it.
- How does a user reset the open in Safari preference for a domain?
- Long-press the link and choose Open in App from the context menu. That flips the per-domain preference back to opening the app. Deleting and reinstalling the app also clears it, but that is a far heavier fix for what is a three-second gesture.
- Why did Universal Links break weeks after we changed anything?
- Apple's CDN caches its copy of your apple-app-site-association file and refreshes on its own schedule. A file broken today can keep working until the CDN next re-fetches, which puts weeks between the change that caused the outage and the outage. Compare the CDN copy at app-site-association.cdn-apple.com/a/v1/yourdomain.com against your origin rather than assuming they match.
- Can a website deploy break my iOS app's deep links?
- Yes, and it is the most common cause of a site-wide regression. Apple's fetcher does not follow redirects, so a new locale prefix, a trailing-slash normaliser, an apex-to-www canonicalisation, a WAF rule, or a CDN configuration change in front of the .well-known path breaks Universal Links without any change to the app.
- Do Universal Links break when an app changes developer accounts?
- Yes. Transferring an app between developer accounts can change the App ID Prefix, which makes the App ID in the deployed apple-app-site-association file stale. The file stays valid JSON and nothing reports an error — it simply names an app identifier that no longer matches the shipped build, so no association is formed.
- Why do Universal Links work for existing users but not new installs?
- Association is fetched and cached when the app is installed, so existing installs are running on a verdict formed before the break. New installs fetch fresh and fail. The reverse pattern, where only existing users are affected, usually means a recent app update shipped without the Associated Domains entitlement.
Related terms
- Universal Links not opening the app — A Universal Link fails to open its app when iOS has no valid association for the domain, when the tapped URL does not match the association's path rules, or when the tap did not originate in a context where iOS honours Universal Links at all.
- AASA file not found — An AASA file not found error means Apple's content delivery network could not retrieve a usable apple-app-site-association file from a domain, which disables Universal Links for that domain entirely.
- Clearing the AASA cache — Clearing the AASA cache means invalidating one of the three separately-expiring copies of a domain's association data: Apple's CDN copy, the device's stored association, and the app's install-time state.