Your Universal Link looks correct, the app is installed, and Safari still opens. That failure usually happens before your navigation code runs: the domain, signed app entitlement, and apple-app-site-association file do not agree exactly.
The `apple-app-site-association` file, commonly called AASA, is extensionless JSON that tells Apple which apps may open which HTTPS URLs for a domain. A working setup requires a valid file on every claimed host, a matching `applinks:` entitlement in the signed app, and app code that safely routes the delivered URL.
This guide builds that chain, tests each layer, and shows how to isolate Safari fallbacks without guessing or repeatedly changing unrelated code.
How apple-app-site-association makes Universal Links work
Universal Links use a two-sided trust relationship. Your app claims a website through the Associated Domains entitlement; the website claims the app through AASA. Apple verifies both claims before iOS lets that app open a matching HTTPS link. Apple describes this as a secure association between the app and website.
That gives an HTTPS URL two useful outcomes:
- If the app is installed and the association matches, iOS can open the relevant in-app content.
- If the app is absent or the URL is outside the allowed rules, the same URL remains a normal web destination.
AASA controls eligibility, not navigation. It can allow /products/42 to reach your app, but your Swift, React Native, or other routing layer must still parse that URL and choose the correct screen.
Think of the full path as four gates:
- The requested URL matches a rule in AASA.
- The AASA app identifier matches the signed build.
- The requested host matches an
applinks:entitlement. - The app accepts the delivered
NSUserActivityand maps it to a safe route.
If a link stays in Safari, test the gates in that order. Debugging screen navigation before iOS has delivered the URL wastes time.
Apple-app-site-association requirements before you write JSON
Collect the exact production values first:
- The fully qualified host users will tap, such as
links.example.com. - The App ID prefix for the signed application, commonly the 10-character Team ID.
- The exact bundle identifier, such as
com.example.store. - The URL paths that should open the app and those that must remain on the web.
- Every app target or environment that should be allowed.
The application identifier placed in AASA is the prefix and bundle identifier joined with a period:
ABCDE12345.com.example.storeDo not assume the App ID prefix from memory. Verify it against the application identifier in the signed build or provisioning profile, especially for an older account or a project with several teams. A correct-looking bundle ID paired with the wrong prefix still fails the association.
Choose hosts deliberately, too. example.com, www.example.com, and links.example.com are different hosts. Apple’s current guidance says each subdomain declared in the entitlement must serve its own association file, and each entitlement entry must name only a domain—not a scheme, path, query, port, or trailing slash. See Apple’s associated-domain configuration requirements.
Create a modern apple-app-site-association file
Create a file named exactly:
apple-app-site-associationIt has JSON content but no .json extension. This example allows product and referral URLs, while deliberately keeping product preview pages on the web:
{
"applinks": {
"details": [
{
"appIDs": [
"ABCDE12345.com.example.store"
],
"components": [
{
"/": "/products/preview/*",
"exclude": true,
"comment": "Keep preview pages on the web"
},
{
"/": "/products/*",
"comment": "Open product pages in the app"
},
{
"/": "/invite/*",
"?": {
"ref": "*"
},
"comment": "Open referral URLs that include a ref value"
}
]
}
]
}
}The appIDs array lets the same rules cover several builds or apps. The components array can match the path with /, query items with ?, and fragments with #; an exclude rule prevents a match from opening the app. Apple’s current `applinks` reference shows all three match types and excluded rules.
Order rules from specific to broad. A preview exclusion placed after /products/* may never protect the preview route because a broad rule can match first. Comments document intent but do not affect routing.
Should you use components or the older paths format?
Use components for a new implementation because it can express path, query, and fragment conditions. You may still encounter the legacy singular appID plus paths form in older projects:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.store",
"paths": [
"/products/*",
"/invite/*"
]
}
]
}
}Do not mechanically mix examples from different eras. Decide which operating-system range your app supports, keep equivalent rules consistent if compatibility requires both representations, and test the oldest supported iOS version. Apple’s archived Universal Links guide documents the original paths behavior, including case-sensitive path matching and first-match ordering.
Keep the file narrow
Avoid starting with a catch-all rule unless the entire website has an app equivalent. A narrow set of product, invitation, order, or account routes is easier to reason about and gives users a useful web fallback for everything else.
Apple’s archived guidance sets an uncompressed AASA limit of 128 KB for modern iOS versions. Prefer a few clear wildcard rules over enumerating thousands of individual URLs.
Host apple-app-site-association without redirects
Publish the file at the current standard location for each claimed host:
https://links.example.com/.well-known/apple-app-site-associationIt must be publicly reachable over HTTPS with a valid certificate and no authentication. Serve the final URL directly rather than redirecting it to www, another hostname, a trailing-slash route, object storage, or a login page. Apple explicitly requires the .well-known location to return without redirects in its supporting associated domains documentation.
Configure the response as JSON. A first-pass origin check is:
curl -i https://links.example.com/.well-known/apple-app-site-associationVerify all of these in the response:
- A final
200status. Content-Type: application/json.- No
Locationheader or redirect hop. - Valid JSON rather than an HTML error document.
- The expected current
appIDsand rules.
Then validate the body independently:
curl -fsS \
https://links.example.com/.well-known/apple-app-site-association \
| python3 -m json.toolRun this from outside your private network. A file that works only through a VPN, allowlisted office IP, preview deployment, or authenticated CDN cannot establish a public association.
Account for Apple’s AASA CDN
Since iOS 14 and macOS 11, installed apps normally obtain association data through an Apple-managed content delivery network rather than fetching your origin directly. Apple says its CDN may request a new domain within 24 hours and devices check periodically afterward.
This creates two states to inspect: what your origin serves now and what Apple’s CDN currently knows. Apple’s debugging technote exposes the CDN response at this pattern:
https://app-site-association.cdn-apple.com/a/v1/links.example.comIf the origin is correct but the CDN response is missing, stale, or reports an error, another app-code change will not fix the handshake. Check DNS, TLS, firewall and bot rules, then allow for propagation. During controlled development, Apple supports alternate modes such as ?mode=developer so an eligible development build can contact a private or changing server directly; do not ship a test-only entitlement by accident.

Add the matching entitlement and app route
In Xcode, open the app target’s Signing & Capabilities tab, add Associated Domains, and enter one item for every host the app claims:
applinks:links.example.comDo not enter https://links.example.com, a URL path, or a trailing slash. Make sure the capability belongs to the target you actually archive—not just a demo, notification extension, or debug target.
Inspect the signed artifact rather than trusting the project editor alone:
codesign -d --entitlements :- /path/to/YourApp.appThe resulting com.apple.developer.associated-domains array should contain the expected applinks: value. This catches stale provisioning, incorrect configurations, and a capability added to the wrong target.
Once iOS accepts the link, it delivers an NSUserActivity whose type is NSUserActivityTypeBrowsingWeb and whose webpageURL contains the HTTPS URL. Apps using scenes receive cold-start and already-running activities through different lifecycle paths, so cover both. Apple’s Universal Link handling guide also recommends validating every URL component and rejecting malformed or dangerous actions.
For deep linking in React Native, the native association is still required. React Navigation recommends its linking configuration so both the initial URL and links received while the app is open update navigation state:
const linking = {
prefixes: ['https://links.example.com'],
config: {
screens: {
Product: 'products/:productId',
Invite: 'invite/:code'
}
}
};
<NavigationContainer linking={linking}>
{/* navigators */}
</NavigationContainer>The React Navigation deep-linking guide covers the associated-domain configuration and the React Native bridge for Universal Links. Test an app that was terminated and one already in memory; a setup that handles only event subscriptions can miss the cold-start URL.
If you would rather not maintain association files, cross-platform routing, install fallback, and campaign context as separate systems, Deeplinkly’s app attribution platform provides branded deep links, deferred routing, and attribution with documented mobile SDKs. The AASA relationship still matters on iOS, but a managed link domain removes much of the hosting and cross-platform operational surface.
Test apple-app-site-association from server to screen
Use a fixed test matrix so every release proves the full path, not just one successful tap.
| Layer | Test | Passing result |
|---|---|---|
| Origin | curl the .well-known URL | Direct 200, JSON content type, expected body |
| Apple CDN | Fetch the Apple CDN URL | Current AASA or an actionable Apple error |
| Signed app | Inspect archived entitlements | Exact applinks:host entry exists |
| Path rules | Test included and excluded URLs | Only intended paths qualify |
| Lifecycle | Tap with app terminated and running | Both states reach the same route |
| Fallback | Test without the app installed | HTTPS page remains useful |
| Safety | Send malformed IDs and query values | App rejects unsafe input without side effects |
On a physical iPhone, place a complete HTTPS link in Notes or Messages, then tap or long-press it. Do not judge the setup by typing the URL into Safari’s address bar: direct browser navigation is expected to remain in the browser. Safari may also keep same-domain links in Safari when the user is already browsing that site, reflecting user intent; Apple documents this behavior.
For an additional device-level check, enable Developer Mode, open Settings → Developer → Universal Links → Diagnostics, and test the full URL. Apple’s TN3155 debugging procedure explains the Notes long-press test, Associated Domains diagnostics, domain matching, and CDN investigation.
When you change AASA, record the origin response, CDN response, app build, device, iOS version, tested URL, and time. That evidence distinguishes propagation delay from a deterministic configuration defect.
Why Universal Links still open Safari
Use the symptom to choose the next check:
| Symptom | Likely cause | Next check |
|---|---|---|
| Every path opens Safari | Host, App ID, entitlement, or CDN association mismatch | Compare origin, CDN, and signed entitlement values character by character |
| One route opens Safari | Path case, order, exclusion, or query rule | Reduce AASA to the failing rule and test the exact URL components |
| Debug works, release fails | Different prefix, bundle ID, capability, or signed entitlement | Inspect both signed builds and AASA app IDs |
| New file works at origin only | CDN has not fetched it or cannot reach it | Read the Apple CDN response and hosting logs |
| App opens to the wrong screen | Native association passed; router mapping failed | Log the received webpageURL and route parser result |
| Same-domain Safari tap stays on web | Browser navigation behavior or prior user choice | Test from Notes or another domain before changing AASA |
Also check these quiet failure modes:
- The server returns
200but the body is an HTML error page. - Middleware rewrites the extensionless filename.
- A wildcard entitlement is assumed to cover the apex host;
*.example.comandexample.comare separate claims. - A staging app ID is absent from the production domain’s AASA.
- A broad component appears before a required exclusion.
- The app parses parameters without validating type, range, authorization, or destination.
Universal Links and Android App Links solve similar problems but use different trust files and platform configuration. Android uses /.well-known/assetlinks.json, application package names, signing-certificate fingerprints, and verified intent filters; do not copy AASA JSON into an Android App Links setup. A cross-platform test plan should verify each operating system independently before testing shared React Native deep linking behavior.
Frequently asked questions
Where should apple-app-site-association be hosted?
Host it at https://your-host/.well-known/apple-app-site-association on every domain or subdomain claimed by the app. The URL must be public HTTPS and return the file directly without a redirect.
Does apple-app-site-association need a .json extension?
No. The filename is exactly apple-app-site-association, without an extension, even though its contents are valid JSON. Configure the server to return an appropriate JSON content type for that extensionless path.
How do I test an AASA file?
Check the origin response and JSON, inspect Apple’s CDN copy, verify the entitlement in the signed app, and tap an included URL from Notes or Messages on a physical device. Test cold start, warm state, excluded paths, no-app web fallback, and malformed parameters.
How long does an AASA update take?
Apple says its CDN requests a file for a new domain within 24 hours and installed devices check for updates periodically, approximately weekly. Developer alternate mode can bypass normal CDN behavior for eligible development testing, but production validation should account for caching.
Can one AASA file support multiple apps?
Yes. Add multiple identifiers to appIDs when the apps share the same component rules, or use separate details entries when their allowed URL sets differ. Include only apps that the domain intentionally authorizes.
Is apple-app-site-association used for Android App Links?
No. AASA is the iOS and Apple-platform association file. Android App Links use assetlinks.json plus Android manifest intent filters and signing-certificate information.
Conclusion: verify the handshake before the router
A reliable Universal Link is not one setting. It is an exact agreement among the requested host, the AASA rules, Apple’s cached association, the signed applinks: entitlement, and the app’s route handler.
Start with one narrow production-like path and prove every layer in the test matrix. Only then expand the rules, add environments, or connect campaign and deferred-deep-link behavior; that sequence turns a silent Safari fallback into a small, observable configuration problem.