# 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.

_Last updated: 2026-10-26_

> **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. Run `curl -sI https://yourdomain.com/.well-known/apple-app-site-association` and expect a `200` with `content-type: application/json`. Then compare Apple's CDN copy at `https://app-site-association.cdn-apple.com/a/v1/yourdomain.com`.

No terminal handy? Paste your domain into the [free AASA validator](https://warplink.app/tools/aasa-validator). For causes outside the file, see [Universal Links Not Working? 15 Causes and the Fix for Each](https://warplink.app/blog/universal-links-not-opening).

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](#step-1-confirm-the-file-is-reachable-and-correct) |
| 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](#step-1-confirm-the-file-is-reachable-and-correct) |
| 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](#step-2-check-what-apples-cdn-actually-cached) |
| 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](#step-3-verify-the-appids-value) and [Step 4](#step-4-verify-the-components-paths) |
| 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](#step-5-verify-the-app-side) |
| 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](#step-6-rule-out-the-device-side-gotchas) |
| 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](#step-7-read-the-device-state-and-the-swcd-logs) |

Sources for every check above: Apple's [TN3155: Debugging universal links](https://developer.apple.com/documentation/technotes/tn3155-debugging-universal-links) and [Supporting associated domains](https://developer.apple.com/documentation/xcode/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`:

```bash
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](https://warplink.app/tools/aasa-validator). It fetches the file the way iOS does and flags each problem below.

You are looking for four things in the response:

1. **Status `200`.** Not `301`, not `302`, not `404`. Apple's CDN does not follow redirects when it fetches your AASA file. If you see a redirect (for example, your server forces `www.` or adds a trailing slash), the fetch fails and Universal Links never work. This is the single most common server-side cause. Run `curl -sIL` if you want to see the full redirect chain, but remember Apple does not follow it.
2. **`content-type: application/json`.** Serving `text/plain`, `text/html`, or `application/pkcs7-mime` (the old signed-AASA format, now unnecessary) will break the fetch on modern iOS. It must be `application/json`.
3. **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 `403` or `404` usually 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`.
4. **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](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html)):

```bash
curl -s https://yourdomain.com/.well-known/apple-app-site-association | wc -c
```

Then validate the body is real JSON:

```bash
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:

```bash
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-4-verify-the-components-paths).

## 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:

```bash
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](https://warplink.app/tools/aasa-generator). The modern AASA format looks like this:

```json
{
  "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.myapp` in `appIDs` silently 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": false` to the component (iOS 13.5 and later).
- **The match is against the path, not the full URL.** Use `/product/*` to match `https://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:

```json
"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:

```bash
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.com` in its Associated Domains capability. Use the bare domain. Do not include `https://`, 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.com` does not cover `www.yourdomain.com` or `links.yourdomain.com`. Per TN3155, a file at `https://yourdomain.com/.well-known/apple-app-site-association` serves `applinks:yourdomain.com` and the wildcard `applinks:*.yourdomain.com`, but not a specific subdomain such as `applinks:www.yourdomain.com`. Each specific subdomain needs its own file on its own host.
- **A port in the domain.** `applinks:yourdomain.com:8080` will 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.com` in Safari and taps a link to another page on `yourdomain.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/login` with its own `applinks:foo.yourdomain.com` entry 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.txt` from 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 +0000
  ```

  `Site/Fmwk Approval: approved` means Apple's CDN approved your links and the pattern matched, so they work. `unspecified` or `denied` means 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 Approval` shows the user's choice to open links in your app or in the browser. `Last Checked` and `Next Check` show 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](https://warplink.app), 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](https://developer.apple.com/documentation/technotes/tn3155-debugging-universal-links): no redirects, `curl` and user-agent checks, `swcutil dl` and `swcutil verify`, `swcutil_show.txt` fields, 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](https://developer.apple.com/documentation/xcode/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](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html): the 128 KB uncompressed size limit for the AASA file.
- The CDN lookup URL and its `Apple-From` header 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](https://warplink.app/blog/universal-links-not-opening), 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](https://warplink.app/blog/deep-linking-guide) covers how Universal Links, App Links, and deferred deep links fit together.
- **Android side:** the assetlinks.json and `autoVerify` equivalent of this checklist is the [Android App Links autoVerify Failed? 12 Causes and Fixes](https://warplink.app/blog/android-app-links-autoverify-failed).
- **Test it properly:** [How to Test Deep Links: simctl and adb Commands, Plus a Pre-Launch Checklist](https://warplink.app/blog/how-to-test-deep-links) 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](https://warplink.app/blog/deep-linking-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](https://warplink.app/blog/firebase-dynamic-links-migration-guide) includes the AASA setup as part of the cutover, and [Firebase Dynamic Links alternatives compared](https://warplink.app/blog/firebase-dynamic-links-alternative) weighs your replacement options.
- **Reference:** the [iOS deep links docs](https://warplink.app/docs/sdks/ios/deep-links) and the [deep linking concepts page](https://warplink.app/docs/concepts/deep-linking).

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](https://warplink.app/docs/concepts/attribution) handles after the link resolves.

Check your file with the [free AASA validator](https://warplink.app/tools/aasa-validator), then [let WarpLink host and update this file](https://dash.warplink.app/auth/signup) so a rule change does not need a server redeploy.
