Apple App Site Association Not Working: The iOS Universal Links Debug Checklist
Universal Links opening Safari instead of your app? Work through this apple-app-site-association debug checklist: AASA fetch, Apple's CDN cache, appIDs, components, and the device-side gotchas.
TL;DR: When Universal Links open Safari instead of your app, the cause is almost always the
apple-app-site-association(AASA) file: it returns a redirect, the wrong content type, or anappIDsvalue missing the Team ID prefix, or Apple's CDN is still serving a stale cached copy. Start with one command:curl -sI https://yourdomain.com/.well-known/apple-app-site-association. If that returns anything other than a200withcontent-type: application/jsonand no redirect, you found it. The rest of this checklist covers the other failure modes, in the order worth checking them.
Why This Is So Hard to Debug
Universal Links fail silently. There is no error dialog, no console warning, no crash. The link just opens in Safari instead of your app, and you are left guessing whether the problem is your AASA file, your entitlement, Apple's cache, or the device itself.
A developer on the Apple Developer Forums described the exact shape of it: "despite having a correctly configured AASA file and associated domains setup, our application does not consistently handle Universal Links and we simply end up getting a blank page." That word, consistently, is the tell. Universal Links are a chain of about a dozen conditions that all have to be true at once, and any single broken link in the chain produces the same symptom: Safari opens, your app does not.
This guide is the checklist. Work it top to bottom. Each step is a command you can run or a value you can check, ordered so the highest-probability causes come first.
Step 1: Confirm the File Is Reachable and Correct
The AASA file must live at exactly this path, served over HTTPS:
https://yourdomain.com/.well-known/apple-app-site-association
Note: no .json extension, and it must be inside /.well-known/. Check it with curl:
curl -sI https://yourdomain.com/.well-known/apple-app-site-association
You are looking for four things in the response:
- Status
200. Not301, not302, not404. Apple's CDN does not follow redirects when it fetches your AASA file. If you see a redirect (for example, your server forceswww.or adds a trailing slash), the fetch fails and Universal Links never work. This is the single most common server-side cause. Runcurl -sILif you want to see the full redirect chain, but remember Apple does not follow it. content-type: application/json. Servingtext/plain,text/html, orapplication/pkcs7-mime(the old signed-AASA format, now unnecessary) will break the fetch on modern iOS. It must beapplication/json.- No authentication. If your server sits behind basic auth, a bot-protection challenge, or a login wall, Apple's CDN gets blocked. The
.well-knownpath must be public. - A valid certificate. The HTTPS certificate has to be valid and trusted. Self-signed or expired certificates fail.
Then validate the body is real JSON and under Apple's 128KB size limit:
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .
If jq errors, your file is not valid JSON. A stray comment, a trailing comma, or a byte-order mark (BOM) at the start of the file will all break parsing. The AASA file is plain JSON with no comments allowed except inside the documented comment fields.
Step 2: Check What Apple's CDN Actually Cached
Here is the part that wastes the most time. iOS does not fetch your AASA file directly from your server for App Store and TestFlight builds. It fetches a copy from Apple's CDN, which crawls your domain and caches the result. You can query that cached copy directly:
curl -s "https://app-site-association.cdn-apple.com/a/v1/yourdomain.com" | jq .
This is the source of truth for what iOS sees. Two things go wrong here:
- The CDN is serving a stale version. After you fix and redeploy your AASA file, Apple's CDN can keep serving the old one for hours, sometimes longer. There is no public way to force a refresh. You wait, and you re-run the command above until the cached copy matches your deployed file.
- The CDN copy is empty or missing. If the CDN URL returns nothing or an error, Apple never successfully fetched your file. That sends you back to Step 1: the fetch is being blocked by a redirect, an auth wall, or a non-200 status.
Bypass the cache during development
You do not have to wait on the CDN while developing. Append ?mode=developer to your Associated Domains entitlement:
applinks:yourdomain.com?mode=developer
Then enable Settings > Developer > Associated Domains Development on the device. In developer mode, iOS fetches the AASA file directly from your server in real time, skipping Apple's CDN entirely. This is the fastest way to test AASA changes without waiting on cache propagation. Remember to remove ?mode=developer before you ship.
Step 3: Verify the appIDs Value
This is the highest-frequency content error inside an otherwise-valid file. The modern AASA format looks like this:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.myapp"],
"components": [
{ "/": "/product/*", "comment": "Product pages" }
]
}
]
}
}
Each entry in appIDs is your Team ID (technically the App ID Prefix, which equals your Team ID for almost every app) followed by a dot followed by your bundle identifier: ABCDE12345.com.example.myapp. Two common mistakes:
- Missing the Team ID prefix. Putting just
com.example.myappinappIDssilently fails verification. One developer traced two days of debugging to exactly this, after inspecting the device logs: "I noticed that my developer Team ID was prepended to the app's bundle id for the denial entry... I added the Team ID as a prefix and all is working." If you only have the bundle ID in there, add the Team ID. - Wrong Team ID. If your app is distributed by a different team than you expect (common with agencies, enterprise accounts, or transferred apps), the Team ID in the AASA must match the team that signed the build, not your personal team. Find the correct value in the Apple Developer portal under Membership, or in the signed app's entitlements.
If you still support iOS 12 and earlier, include the legacy appID (singular) and paths keys alongside the modern appIDs and components. Modern iOS reads components; older iOS reads paths. Serving both is safe.
Step 4: Verify the components Paths
The components array decides which URLs your app claims. Three things trip people up:
- Paths are case-sensitive by default. A component of
/Product/*will not match a link to/product/123. To opt into case-insensitive matching, add"caseSensitive": falseto the component (iOS 13.5 and later). - The match is against the path, not the full URL. Use
/product/*to matchhttps://yourdomain.com/product/123. A leading*like*/product/*behaves differently and is rarely what you want. - Exclusions need
"exclude": true. If you want everything except/blog/*to open the app, list the exclusion first, because components are evaluated top to bottom and the first match wins:
"components": [
{ "/": "/blog/*", "exclude": true },
{ "/": "/*" }
]
A catch-all { "/": "/*" } claims every path on the domain, which is usually too broad. Claim only the paths your app actually routes.
Step 5: Verify the App Side
If the file is perfect and the CDN copy is current, the problem is in the app build itself.
- Associated Domains entitlement. Your app must declare
applinks:yourdomain.comin its Associated Domains capability. Use the bare domain. Do not includehttps://, a path, or a port. List each domain you serve links from as a separate entry. - The capability must be in the provisioning profile. Adding the entitlement in Xcode is not enough on its own. The provisioning profile used to sign the build has to include the Associated Domains capability. If you enabled it after generating the profile, regenerate the profile and re-sign.
- Subdomain mismatch.
applinks:yourdomain.comdoes not coverwww.yourdomain.comorlinks.yourdomain.com. The AASA file has to be reachable on the exact host in the entitlement, and the entitlement host has to match the host in your links. On iOS 14 and later you can use a wildcard,applinks:*.yourdomain.com, to cover subdomains.
Step 6: Rule Out the Device-Side Gotchas
These are the failures that make Universal Links look intermittent, because they depend on how the link was tapped, not on your configuration.
- iOS refetches the AASA at install, on app update, and only about once a week after that. Editing your file does not promptly update a device that already has the app. To pick up AASA changes right away, delete the app and reinstall it (or use
?mode=developer). Testing edits against an already-installed build is the most common false negative in this entire checklist. - Same-domain taps do not trigger Universal Links. If the user is already on
yourdomain.comin Safari and taps a link to another page onyourdomain.com, iOS treats it as in-page navigation and never opens the app. This is by design, and it is why deep linking services use a separate short-link domain. - Paste-and-go does not trigger Universal Links. Typing or pasting the URL into Safari's address bar is direct navigation, not a link tap. Universal Links only fire when the user taps an actual link. Test with a real tappable link (in Notes or Messages), not the address bar.
- The user can disable it. After your app opens a link, iOS shows the site name as a breadcrumb in the top corner. If the user taps it to view the page in Safari instead, iOS remembers that and stops auto-opening the app for that domain. To re-enable, long-press a link and choose to open it in the app. There is no API to detect or reset this.
- iCloud Private Relay is usually a red herring here. Private Relay masks the device IP, which degrades IP-based deferred deep link attribution, a separate system. It does not break the AASA fetch (which travels over Apple's own infrastructure) or Universal Link routing. If your links open Safari, Private Relay is almost never the cause. Check the file and the entitlement first.
- In-app browsers. Links tapped inside apps like Instagram, X, or Facebook often open in an embedded web view that ignores Universal Links entirely. This is the embedding app's behavior, not your AASA. There is no AASA fix for it; the path forward is a smart redirect or a deferred deep link.
Step 7: Read the swcd Logs
When the checklist above does not surface it, go to the daemon that does the work. Universal Link association is handled by swcd (the Shared Web Credentials daemon), and it logs why a verification failed.
- Live logs. Connect the device to a Mac, open Console.app, select the device, and filter the process column for
swcd. Tap a link and watch the entries. They tell you whether the file was fetched, parsed, and matched. - swcutil_show.txt. Capture a sysdiagnose on the device (hold both volume buttons and the side button briefly), then open
swcutil_show.txtfrom the archive. It lists each app's associated-domains approval state. ADeniedentry, and theappIDprinted next to it, is exactly how the Team-ID-prefix bug in Step 3 gets caught.
If you see an SWCErrorDomain error in those logs, treat it as a pointer back into this checklist rather than a final answer: it generally means the device could not fetch or validate the AASA, so re-check reachability (Step 1), the CDN copy (Step 2), and whether an MDM or enterprise-management profile is interfering.
The Short Version
Universal Link opens Safari, not the app
│
▼
curl -sI the AASA URL ───► not 200 / redirect / wrong content-type? ──► fix the server (Step 1)
│ ok
▼
curl the Apple CDN copy ──► stale or empty? ──► wait for cache, or use ?mode=developer (Step 2)
│ matches
▼
appIDs = TeamID.bundleID? ──► no ──► add the Team ID prefix (Step 3)
│ yes
▼
components match the path, case included? ──► no ──► fix the paths (Step 4)
│ yes
▼
entitlement + provisioning profile correct? ──► no ──► re-sign (Step 5)
│ yes
▼
reinstall the app, tap a real link, Private Relay off (Step 6)
│ still failing
▼
read swcd logs / swcutil_show.txt (Step 7)
How WarpLink Removes Half of This Checklist
Steps 1 through 4 are all about hosting a byte-perfect AASA file: right path, right content type, no redirects, valid JSON, correct appIDs, sensible components. That is the half of the checklist that has nothing to do with your app and everything to do with your server.
When you register an app with WarpLink, it generates the AASA file from your Team ID and bundle ID and serves it for you, at the edge, with Content-Type: application/json, no redirects, and no auth wall, on both your WarpLink link domain and any custom domain you add. The appIDs value is assembled from the Team ID you entered, so the most common content error in Step 3 cannot happen. You are left with only the app-side and device-side steps, which are yours to own regardless of who hosts the file.
This guide is vendor-neutral on purpose. Whether you host the AASA yourself or let a service do it, the checklist is the same. WarpLink just deletes the rows that come from getting the hosting wrong.
Frequently Asked Questions
Why is my AASA file not being downloaded?
Almost always a redirect, a non-200 status, an auth wall, or a wrong content type on the /.well-known/apple-app-site-association URL. Run curl -sI against it and confirm a clean 200 with application/json. Then check Apple's CDN copy at https://app-site-association.cdn-apple.com/a/v1/yourdomain.com to see what iOS actually receives.
Why does the AASA file not update after I push a change?
Apple's CDN caches it, and App Store and TestFlight builds read the cached copy, not your server. There is no public cache-purge. Either wait for the cache to refresh (re-query the CDN URL until it matches) or use applinks:yourdomain.com?mode=developer with Developer mode enabled to fetch directly from your server during testing.
Should the AASA file be served over http/1.1 or http/2?
Either works. Content type, status code, and reachability matter; the HTTP version does not. If you suspect a protocol-specific proxy issue, test the raw fetch with curl -sI and confirm a 200.
Do I need the .well-known/ directory for AASA?
Yes. Modern iOS only looks in https://yourdomain.com/.well-known/apple-app-site-association. The legacy root path is no longer reliable. Serve it from .well-known.
My AASA is correct but links still open Safari. What now?
Reinstall the app to force an immediate AASA refetch, tap a real link rather than pasting into the address bar, and make sure you are not tapping a same-domain link or testing inside an in-app browser. Then read the swcd logs in Console.app.
Related Guides
- Broader symptom: if you are not sure the file is the problem, start with Universal Links Not Opening: Every Cause and How to Fix Each One, which triages the file, the entitlement, and the app-side handler.
- Start here: Deep Linking: The Complete Guide for Mobile Developers covers how Universal Links, App Links, and deferred deep links fit together.
- Android side: the assetlinks.json and
autoVerifyequivalent of this checklist lives in the deep linking guide's App Links section. - Migrating off Firebase Dynamic Links: the Firebase Dynamic Links Migration Guide includes the AASA setup as part of the cutover.
- Reference: the iOS deep links docs and the deep linking concepts page.
Once your Universal Links open the app reliably, the next question is usually attribution: which link, campaign, or channel actually drove the open and the install. That is a different problem, and it is what WarpLink handles after the link resolves.
Create a free WarpLink account and let the AASA file host itself, so you can get back to the half of this checklist that is actually about your app.
WarpLink Team
Building the open deep linking platform for developers and small teams.
Related Posts
Android App Links autoVerify Failed: The assetlinks.json Verification Debug Checklist
App Links opening a chooser dialog instead of your app? Work through this assetlinks.json autoVerify debug checklist: the Play App Signing SHA-256 trap, package_name, the intent-filter, multi-host cascade, and the adb verification commands.
Deep Linking: The Complete Guide for Mobile Developers
What is deep linking? Learn how universal links, app links, and deferred deep links work. Covers iOS, Android, and cross-platform implementation.
Deferred Deep Linking on iOS: How to Implement It in Swift
A vendor-neutral Swift walkthrough of deferred deep linking on iOS: why iOS has no native support, the match cascade (IDFV, fingerprint, raw signals), the ATT and Private Relay reality, and a one-call first-launch implementation.