Skip to content
CodeOnAir
← All posts

Universal Links not working? An iOS debugging checklist

When a link opens Safari instead of your app, the cause is in one of four places: the AASA file, Apple's CDN, your app's entitlement, or how you're testing. Here's how to check each one.

Super Admin22 October 20269 min read

You tap a link and Safari opens instead of your app. There's no error message: iOS falls back to the browser without saying why. That's why Universal Links are hard to debug.

The good news is that the cause is almost always in one of four places:

  1. The apple-app-site-association (AASA) file on your domain.
  2. Apple's CDN, which is where iPhones actually read that file.
  3. Your app: the Associated Domains entitlement and the code that receives the link.
  4. How you're testing: some ways of opening a link never open an app.

Work through them in order. Each section ends with a command or a setting you can check in a minute or two.

How Universal Links decide whether to open your app

Here's what iOS does:

  • When your app is installed (and periodically after that), iOS reads the AASA file for every applinks: domain in your app's entitlement. Since iOS 14 it reads the file from Apple's CDN, not from your server.
  • When someone taps an https:// link on one of those domains, iOS checks the path against the components in the file. If it matches and the user hasn't opted out, the app opens. Otherwise, the browser opens.

So a link can only open your app if all of these are true: the file is reachable and valid, Apple's CDN has a current copy, the app lists the exact domain, and the path matches. If one is false, the link opens in Safari.

1. Check the apple-app-site-association file

Start with the raw response, the way Apple's crawler sees it:

curl -v https://links.example.com/.well-known/apple-app-site-association

Check each of these:

  • HTTP 200, over HTTPS, with a valid certificate. A self-signed or incomplete certificate chain fails silently.
  • No redirects. A 301 or 302 (for example, example.com → www.example.com, or a login wall) means the file isn't used. Host the file on every domain and subdomain you list in the app.
  • The exact path. /.well-known/apple-app-site-association, with no .json extension.
  • JSON content type. Serve it as application/json. A common trap: single-page apps return their index.html with a 200 for unknown paths, so the "file" is really HTML.
  • No IP or user-agent blocking. Apple's technote says the file must be reachable from any IP address and any user agent. Bot protection (Cloudflare, AWS WAF, "under attack" modes) can block Apple's crawler while your browser works fine. Test with a random user agent:
curl -A "MyAgent-Bot/1.0" https://links.example.com/.well-known/apple-app-site-association

What a correct AASA file looks like

Apple recommends the appIDs + components format (iOS 13 and later):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.example.shop"],
        "components": [
          { "/": "/product/*", "comment": "Product pages" },
          { "/": "/account/*", "exclude": true, "comment": "Keep account pages in the browser" },
          { "/": "/*" }
        ]
      }
    ]
  }
}

Things that commonly go wrong in the file:

  • Wrong app ID. It's TEAMID.bundleid. The Team ID is the 10-character ID under Membership details on developer.apple.com, not your name and not the App Store ID. The bundle ID must match the build you installed, including .dev or .staging suffixes.
  • Mixing formats. Use either appIDs + components, or the older appID + paths. Apple advises against mixing them in one entry.
  • Order matters. Components are checked top to bottom and the first match wins. Put an exclude rule before a catch-all /*, or the catch-all matches first.
  • Patterns that don't match your real URLs. /product/* doesn't match /products/42. Paste a full link into a validator rather than reading patterns by eye.

Verify the file and a path with swcutil (macOS)

macOS ships Apple's own tool for this. Download the file and test a URL against it:

sudo swcutil dl -d links.example.com
curl -s https://links.example.com/.well-known/apple-app-site-association -o aasa.json
sudo swcutil verify -d links.example.com -j ./aasa.json -u https://links.example.com/product/42

A matching path prints Pattern "https://links.example.com/product/42" matched. An excluded one prints blocked match. If a URL you expect to open the app says neither, your components don't cover it.

2. Check what Apple's CDN has

This is the step most guides skip, and it explains a lot of "the file is fine but it still doesn't work" reports.

Since iOS 14, devices don't download the AASA file from your server. They get it from an Apple-managed CDN, which fetches your file and caches it. You can see exactly what devices see:

curl -s https://app-site-association.cdn-apple.com/a/v1/links.example.com

Compare that with your server's file. If they differ, phones are still using the old copy. According to Apple's technote, devices download the file when the app is installed and check for updates about once a week after that. There's no way to purge the CDN. To pick up a new version on a test device, delete and reinstall the app once the CDN has the new file.

If the CDN returns an error or nothing at all, it couldn't fetch your file. Go back to step 1 and look at redirects, firewalls and certificates.

Skipping the CDN during development

When you're testing against a staging server, or changing the file faster than the CDN refreshes, use developer mode:

  1. Add ?mode=developer to the entitlement: applinks:staging.example.com?mode=developer.
  2. On the iPhone, turn on Developer Mode, then go to Settings → Developer and turn on Associated Domains Development.
  3. Build and run from Xcode. The device now fetches the file directly from your server.

Remove ?mode=developer before you submit to the App Store; it's for development builds only.

3. Check the app: entitlement and link handling

The Associated Domains entitlement

In Xcode: your target → Signing & Capabilities → Associated Domains. Each domain needs its own line:

applinks:links.example.com
applinks:example.com

Common mistakes:

  • Including the protocol. It's applinks:links.example.com, not applinks:https://links.example.com.
  • Expecting the root domain to cover subdomains. applinks:example.com doesn't cover www.example.com. List each subdomain, or use applinks:*.example.com, which reads the file from the root domain.
  • Using a port. applinks:example.com:8080 never works.
  • The capability isn't in the signed build. It's easy to add it to one target or configuration and ship another. Check the actual build:
codesign -d --entitlements :- /path/to/YourApp.app

Look for com.apple.developer.associated-domains with your domains in the output. The App ID on developer.apple.com also needs the Associated Domains capability turned on, and your provisioning profile must include it.

Receiving the link in code

If the app opens but lands on the home screen, the link reached iOS but not your code.

  • SwiftUI: use .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in ... } and read activity.webpageURL.
  • UIKit with scenes: links arrive in scene(_:continue:) and, on a cold start, in connectionOptions.userActivities inside scene(_:willConnectTo:options:). Implementing only the AppDelegate method is a classic bug in apps that use scenes.
  • React Native: forward the activity to RCTLinkingManager in your AppDelegate, then read the URL with Linking.getInitialURL() and Linking.addEventListener("url", ...):
func application(
  _ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  return RCTLinkingManager.application(
    application, continue: userActivity, restorationHandler: restorationHandler)
}

Our React Native deep linking guide covers the JavaScript side.

4. Check how you're testing

Several ways of "testing a link" never open an app, even when everything is set up correctly:

  • Typing or pasting the URL into Safari's address bar. That's direct navigation in the browser, and it never opens the app.
  • Tapping a link on the same domain. If you're already on example.com in Safari and tap another example.com link, Safari keeps you in the browser. Apple suggests a separate subdomain (such as links.example.com) for links that should open the app from your own site.
  • The user opted out. If you long-press a link and choose "Open in Safari", iOS remembers that choice for the domain. Long-press a link again and choose "Open in [App]" to reset it.
  • In-app browsers. Instagram, Facebook and some other apps open links in their own web view, which often doesn't trigger Universal Links. You need a fallback page with an "Open in app" button for those.

The reliable test: paste the link into Notes or Messages, then tap it. If you long-press it, you should see an option to open it in your app.

Use the on-device diagnostics

iOS has a built-in checker. With Developer Mode on, go to Settings → Developer → Universal Links → Diagnostics and enter your full URL. It tells you whether the link is valid for an installed app.

For harder cases, Apple's technote suggests a sysdiagnose: open swcutil_show.txt and search for your app ID. Site/Fmwk Approval: approved means the CDN approved the association. denied or unspecified points back to hosting or the entitlement. User Approval shows whether the user opted out.

A 2-minute checklist

Check How
File returns 200, JSON, no redirect curl -v https://domain/.well-known/apple-app-site-association
App ID is TEAMID.bundleid for this build Membership details + Xcode bundle ID
Your path matches components sudo swcutil verify -d … -j … -u …
Apple's CDN has the current file curl https://app-site-association.cdn-apple.com/a/v1/domain
Entitlement lists the exact domain, no https:// codesign -d --entitlements :- YourApp.app
App was reinstalled after the file changed Delete and reinstall
Tested by tapping, not typing Notes or Messages
Code handles the link (scene / RN Linking) Log webpageURL

Let a tool check it for you

Our free Universal Link & App Link Validator runs most of these server-side checks at once: HTTPS, redirects, content type, JSON structure, app ID format, whether your exact path matches, and whether Apple's CDN copy matches your server. It also checks Android's assetlinks.json; if Android is giving you trouble too, see Android App Links not verified? Fix assetlinks.json.

If you'd rather not host and maintain these files at all, CodeOnAir AppLinks builds apple-app-site-association and assetlinks.json from your app details and serves them on every link domain, your free yourapp.codeonair.com subdomain or your own domain. Adding ?d=1 to any link opens a link debugger that shows where each platform would go. The AppLink setup docs walk through it. AppLinks is on the paid plans (pricing); sign-ups are currently by invitation, so request access.

Need a tester's device ID for an ad hoc build while you're at it? Get UDID finds it in Safari.

FAQ

Why do my Universal Links open in Safari instead of the app?

iOS falls back to Safari when it can't confirm the association. That happens when the AASA file is unreachable, redirects or is invalid, when Apple's CDN has an old copy, when the app's Associated Domains entitlement doesn't list that exact domain, or when the path isn't in components. It also happens if you type the URL into Safari, or if the user once chose "Open in Safari" for that domain.

How long does Apple take to pick up a new apple-app-site-association file?

Apple's CDN fetches and caches the file; there's no manual purge. Devices download it when the app is installed and check about once a week after that. For testing, reinstall the app after the CDN has the new version, or use ?mode=developer with Associated Domains Development turned on.

Does the apple-app-site-association file need a .json extension?

No. Serve it at /.well-known/apple-app-site-association with no extension, over HTTPS, with an application/json content type and no redirects.

Do I need an AASA file on www and the root domain?

Yes, if you link from both. Each domain or subdomain listed in your entitlement needs its own file at its own /.well-known/ path. The exception is a wildcard entry (applinks:*.example.com), which reads the file from the root domain.

Can I test Universal Links in the iOS Simulator?

Yes. Run xcrun simctl openurl booted https://links.example.com/product/42 with your app installed, or tap the link in Notes in the simulator. The simulator also reads the file through Apple's CDN unless you use ?mode=developer.

Why do Universal Links work in Notes but not in Instagram?

Instagram and Facebook open links in their own in-app browser, which often doesn't hand Universal Links to iOS. A landing page with an "Open in app" button (or a custom URI scheme) is the usual workaround.


Checked 2026-10-22 against Apple's TN3155 "Debugging universal links" (revision dated 2025-05-15) and "Supporting associated domains". iOS behavior can change between releases; if a step doesn't match what you see, Apple's technote is the reference.

Your first AppLink in five minutes.

Create an app, pick a subdomain, and share a link that opens your app. Try Pro free for 30 days — no card needed.