Universal Links Open Safari? Fix Your apple-app-site-association File
Universal Links opening Safari? Run curl -sI on the AASA file, compare Apple's CDN copy, then fix appIDs, paths and entitlements. Seven steps, one check each.
TL;DR: Universal Links open Safari when iOS cannot use your
apple-app-site-association(AASA) file. The top cause is a redirect, a non-200 status, or a wrong content type. Runcurl -sI https://yourdomain.com/.well-known/apple-app-site-associationand expect a200withcontent-type: application/json. Then compare Apple's CDN copy athttps://app-site-association.cdn-apple.com/a/v1/yourdomain.com.
No terminal handy? Paste your domain into the free AASA validator. For causes outside the file, see Universal Links Not Working? 15 Causes and the Fix for Each.
If the curl returns anything other than a 200 with content-type: application/json and no redirect, you found it. The checklist below covers the other failure modes in the order worth checking them, including an appIDs value missing the Team ID prefix and Apple's CDN serving a stale cached copy.
Fault-Isolation Table: Find the Failing Layer First
Universal Links pass through four layers: your server, Apple's CDN, the app's entitlement, and the device. Run the checks in this table from top to bottom. The first row whose result does not match is the failing layer.
| Symptom | Check | Expected result | Next fix |
|---|---|---|---|
| The CDN has no copy, or iOS never downloads the file | curl -sI https://yourdomain.com/.well-known/apple-app-site-association | 200, content-type: application/json, no Location header | Remove the redirect, auth wall or bot challenge. See Step 1 |
| The server is fine but the file blocks some visitors | curl -A "MyAgent-Bot/*" https://yourdomain.com/.well-known/apple-app-site-association | Same JSON as a normal request | Allow all IP ranges and all user agents on /.well-known. See Step 1 |
| You fixed the file and nothing changed | curl -s "https://app-site-association.cdn-apple.com/a/v1/yourdomain.com" | The body matches the file you deployed | Wait until the CDN copy matches the deployed file, then reinstall the app. Use ?mode=developer while testing. See Step 2 |
| The file loads but the app is never offered | swcutil verify -d yourdomain.com -j ./aasa.json -u https://yourdomain.com/path | Pattern ... matched. | Fix appIDs (Team ID prefix) and components. See Step 3 and Step 4 |
| The AASA is right and the build still fails | Xcode, Signing & Capabilities: Associated Domains | applinks:yourdomain.com, no scheme, path or port | Fix the entitlement and re-sign. See Step 5 |
| Everything above passes on a device | Settings > Developer > Associated Domains Development > Diagnostics, then enter the full URL | Feedback that the link is valid for an installed app | Re-test with a tapped link after a reinstall. See Step 6 |
| You need the device's own verdict | sysdiagnose, then swcutil_show.txt | Site/Fmwk Approval: approved next to your App ID | unspecified or denied means Apple's CDN could not fetch or match the file. See Step 7 |
Sources for every check above: Apple's TN3155: Debugging universal links and Supporting associated domains.
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
To run these checks without a terminal, paste your domain into our free AASA validator. It fetches the file the way iOS does and flags each problem below.
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, and no blocked visitors. If your server sits behind basic auth, a bot-protection challenge, or a login wall, Apple's CDN gets blocked. A
403or404usually means the path is not reachable from every IP address. Apple does not publish its CDN address ranges, so the path must be public from anywhere, and the server must answer any user agent. Test that with a made-up one:curl -A "MyAgent-Bot/*" https://yourdomain.com/.well-known/apple-app-site-association. - A valid certificate. The HTTPS certificate has to be valid and trusted. Self-signed or expired certificates fail.
Then check the size. The AASA file must be at most 128 KB uncompressed (iOS 9.3.1 and later, per Apple's Universal Links guide):
curl -s https://yourdomain.com/.well-known/apple-app-site-association | wc -c
Then validate the body is real JSON:
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.
Apple's note on hosting is short: the file must be served over https:// with a valid certificate and with no redirects. If the host redirects (for example the apex to www), host a copy of the file on every domain and subdomain listed in your applinks entries instead of redirecting.
Check the file on a Mac with swcutil
macOS ships swcutil, the same tool Apple's TN3155 uses. Run both commands with sudo. The first downloads the file the way the system does. The second checks a saved copy against a URL, and prints which component matched:
sudo swcutil dl -d yourdomain.com
sudo swcutil verify -d yourdomain.com -j ./aasa.json -u https://yourdomain.com/product/123
A match prints Pattern "https://yourdomain.com/product/123" matched. An exclusion prints Pattern "..." blocked match. If you see blocked match for a path you did not exclude, the pattern did not match. Go to Step 4.
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. Add -D - and -o /dev/null to read the headers instead of the body. At the time of writing, the response carries an Apple-From header that names the URL Apple fetched, and Cache-Control: max-age=3600. 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. Apple states that the CDN requests your file within 24 hours, that devices check for updates about once a week after install, and that there is no direct CDN invalidation. Re-run the command above until the cached copy matches your deployed file, then reinstall the app on the device.
- 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 (Developer Mode must be on first). In developer mode, iOS pulls the AASA file directly from your domain and bypasses Apple's CDN. This is the fastest way to test AASA changes without waiting on cache propagation, and it also covers a server that is not reachable from the public internet. Remove ?mode=developer before you ship.
Step 3: Verify the appIDs Value
This is the highest-frequency content error inside an otherwise-valid file. To build the file from your Team ID and bundle ID instead of typing it, use our AASA file generator. 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.
Use one format per file. Apple's guidance is appIDs with components (the format shown above), or the legacy appID (a string) with paths (an array). Apple tells you to avoid mixing the two, because doing so may cause unexpected behavior. If you must support the legacy format, ship it as a separate details entry and test it on a device that runs that iOS version.
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.
You can test the rules on a Mac before you deploy the file. Save a local copy of the AASA file and run swcutil verify against a test URL:
sudo swcutil verify -d yourdomain.com -j ./apple-app-site-association.json -u https://yourdomain.com/product/123
The output says Pattern "https://yourdomain.com/product/123" matched. or blocked match. for each URL you try, so a case or exclusion mistake shows up in seconds instead of after a CDN refresh.
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. Per TN3155, a file athttps://yourdomain.com/.well-known/apple-app-site-associationservesapplinks:yourdomain.comand the wildcardapplinks:*.yourdomain.com, but not a specific subdomain such asapplinks:www.yourdomain.com. Each specific subdomain needs its own file on its own host. - A port in the domain.
applinks:yourdomain.com:8080will not open your app. Use the bare host only.
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 fetches the AASA at install, and checks for updates 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. Apple's own test: paste the link into Notes and long-press it. If the file is configured correctly, the menu offers both your app and the browser.
- Same-domain fix. To open the app from a link on your own site, put the link on a different subdomain than the page that holds it. TN3155 recommends this, for example a login button that points to
https://foo.yourdomain.com/loginwith its ownapplinks:foo.yourdomain.comentry and AASA file. - 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. 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 Device State and the swcd Logs
When the checklist above does not surface it, ask the device. Universal Link association is handled by swcd (the Shared Web Credentials daemon), and it records why a verification failed.
-
swcutil_show.txt. Capture a sysdiagnose on a device that has the app installed, then open
swcutil_show.txtfrom the archive and search for your App ID. A healthy entry looks like this (from TN3155):Service: applinks App ID: 1234abcd.com.example Domain: example.com User Approval: unspecified Site/Fmwk Approval: approved Last Checked: 2023-08-24 10:09:00 +0000 Next Check: 2023-08-18 21:00:19 +0000Site/Fmwk Approval: approvedmeans Apple's CDN approved your links and the pattern matched, so they work.unspecifiedordeniedmeans the CDN could not fetch the file or did not match it. Allow all IP addresses, confirm the domain is in the entitlement, and re-check Step 1.User Approvalshows the user's choice to open links in your app or in the browser.Last CheckedandNext Checkshow when the CDN last requested your file and when it asks again. -
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.
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 until it matches, then reinstall; use ?mode=developer to test (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
▼
swcutil_show.txt + swcd logs (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 the app-side and device-side steps, Steps 5 through 7.
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 devices read the cached copy, not your server. There is no public cache-purge. Apple states that the CDN requests your file within 24 hours of an install and that devices check for updates about once a week, so re-query the CDN URL until it matches, then reinstall the app. For testing, use applinks:yourdomain.com?mode=developer with Associated Domains Development enabled to fetch directly from your server.
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 run sudo swcutil dl -d yourdomain.com and swcutil verify on a Mac, and read swcutil_show.txt from a sysdiagnose for Site/Fmwk Approval.
How do I test AASA path matching without a device?
On a Mac, run sudo swcutil verify -d yourdomain.com -j ./apple-app-site-association.json -u https://yourdomain.com/some/path. It reports whether the URL matched a component or was blocked by an exclusion, so you can check your components before you deploy the file.
Sources
- Apple, TN3155: Debugging universal links: no redirects,
curland user-agent checks,swcutil dlandswcutil verify,swcutil_show.txtfields, CDN caching and the weekly update check, the Associated Domains Development diagnostics, wildcard and subdomain file placement, and the same-domain navigation rule. - Apple, Supporting associated domains: file path, HTTPS with a valid certificate and no redirects, the CDN introduced in iOS 14 and macOS 11, the
?mode=entitlement suffix, and the 24 hour first fetch. - Apple, Universal Links: the 128 KB uncompressed size limit for the AASA file.
- The CDN lookup URL and its
Apple-Fromheader were checked against a live domain in October 2026.
Related Guides
- Broader symptom: if you are not sure the file is the problem, start with Universal Links Not Working? 15 Causes and the Fix for Each, which triages the file, the entitlement, and the app-side handler.
- Start here: What Is Deep Linking? How Mobile Deep Links Work on iOS and Android covers how Universal Links, App Links, and deferred deep links fit together.
- Android side: the assetlinks.json and
autoVerifyequivalent of this checklist is the Android App Links autoVerify Failed? 12 Causes and Fixes. - Test it properly: How to Test Deep Links: simctl and adb Commands, Plus a Pre-Launch Checklist covers
swcutil, the Console diagnostics, and the testing mistakes that make a correct file look broken. - The file is correct and it still fails in one app: Deep Links in Instagram and Facebook In-App Browsers covers the in-app browser cases, where the AASA file is never the problem.
- Migrating off Firebase Dynamic Links: the Firebase Dynamic Links Migration Guide includes the AASA setup as part of the cutover, and Firebase Dynamic Links alternatives compared weighs your replacement options.
- 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.
Check your file with the free AASA validator, then let WarpLink host and update this file so a rule change does not need a server redeploy.
Get your first deep link working today
Deep linking, install attribution, and real-time analytics in one SDK. Free for 10K clicks a month, no sales call.
WarpLink is link infrastructure that sends one link to the right place on iOS, Android, and the web, with install attribution and real-time analytics on every tap.
Stuck on setup? Email support@warplink.app and we will help you get one real link working.
WarpLink Team
Building affordable, reliable link infrastructure for mobile teams. Deep linking, install attribution, and real-time analytics in one SDK.
Related Posts
Universal Links Not Working? 15 Causes and the Fix for Each
Universal Links not working or opening Safari? Fifteen causes in order, from address-bar tests to AASA files and handlers, each with the check and the fix.
Deep Links in Instagram and Facebook In-App Browsers: The Fixes
A deep link tapped in the Instagram, Facebook, or TikTok in-app browser usually will not open your app. What each one does on iOS and Android, and the fixes.
Email Deep Linking: Why Tracked Links Open the Browser, Not Your App
Email click tracking rewrites links to a tracking domain, so Universal Links never fire. See the mechanism, three fixes, and provider support.