guides

The AASA file, explained

the linkboo team·7 min read·updated Mon Jun 01 2026 17:00:00 GMT-0700 (Pacific Daylight Time)
On this page

scope

The complete reference for the Apple App Site Association file. Every field documented, every gotcha enumerated. This is the format spec — for the setup walkthrough, see /guides/ios-universal-links-setup. For the Android equivalent, see /guides/assetlinks-json-explained.

Cluster C hub: /guides/firebase-dynamic-links-replacement.

the file

A JSON file served at:

https://yourdomain.com/.well-known/apple-app-site-association

The file declares which iOS apps are authorized to handle URLs on your domain. iOS fetches it once at app install time (and periodically afterward via Apple's CDN proxy) and uses the declared associations to route Universal Link taps.

Hosting requirements:

Property Required value
Scheme https://
Path /.well-known/apple-app-site-association
Extension None — do not append .json
Content-Type application/json (case-insensitive, charset suffix OK)
Redirects None — 301/302 cause iOS to reject the file
File size Under 128 KB (Apple cap)
Authentication None — public anonymous GET
TLS Publicly trusted certificate (no self-signed)

the modern format (iOS 13+)

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCD1234EF.com.yourcompany.yourapp"],
        "components": [
          {
            "/": "/product/*",
            "comment": "Product detail pages"
          },
          {
            "/": "/profile/*",
            "exclude": true,
            "comment": "Profile pages excluded from Universal Links"
          },
          {
            "/": "/article/*",
            "?": { "preview": "true" }
          }
        ]
      }
    ]
  }
}

The structure:

  • applinks.details[] — array of app associations. Multiple entries allow different apps to claim different paths on the same domain.
  • appIDs[] — Team ID + bundle ID. Multiple app IDs supported per details entry.
  • components[] — path matching rules. Evaluated in order; first match wins.

Each component entry can have:

Key Type Meaning
/ string URL path pattern. * matches one or more path components; ? matches a single character
? object Query parameter filter — link must match these key-value pairs
# string Fragment filter
exclude boolean If true, this match REMOVES the URL from Universal Link handling
comment string Documentation, ignored by iOS
caseSensitive boolean Default true. Set false for case-insensitive matching
percentEncoded boolean Default true. Set false to compare the unencoded path

The components[] array gives you fine-grained control over which URLs route to the app vs which fall through to Safari.

a complete example

A real-world AASA for a media site:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCD1234EF.com.example.reader"],
        "components": [
          {
            "/": "/article/*",
            "comment": "All articles open in the app"
          },
          {
            "/": "/article/preview/*",
            "exclude": true,
            "comment": "But preview links stay on web"
          },
          {
            "/": "/podcast/*",
            "comment": "Podcast episodes open in the app"
          },
          {
            "/": "/author/*",
            "?": { "share": "true" },
            "comment": "Author pages open in app ONLY when share=true is present"
          },
          {
            "/": "/admin/*",
            "exclude": true,
            "comment": "Admin paths never open in app"
          }
        ]
      },
      {
        "appIDs": ["ABCD1234EF.com.example.podcast"],
        "components": [
          {
            "/": "/podcast/*",
            "comment": "Podcast app ALSO claims podcast routes"
          }
        ]
      }
    ]
  }
}

Order matters in components. The exclude: true entry for /article/preview/* must come AFTER the broader /article/* entry — otherwise the broader rule wins and previews open in the app. iOS evaluates top-to-bottom and stops at the first match.

When two apps claim overlapping paths (as the reader and podcast apps do above for /podcast/*), iOS surfaces the system app-picker on first tap and remembers the user's choice.

the legacy format (pre-iOS 13)

Older Apple documentation showed a simpler format:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABCD1234EF.com.yourcompany.yourapp",
        "paths": ["/product/*", "NOT /admin/*"]
      }
    ]
  }
}

Differences:

  • appID (singular) instead of appIDs[].
  • paths[] — array of strings. NOT /path syntax for exclusions.
  • apps[] — vestigial empty array, ignored.

Apple still honors this format for backwards compatibility, but new files should use the modern components syntax. iOS 13+ provides richer matching (query filters, fragment filters, case sensitivity) via components that paths cannot express.

You can include both formats in a single file for transitional support, but iOS 13+ ignores the legacy paths if components is present.

defaults inheritance

In the modern format, the defaults object lets you avoid repeating the same flag on every component:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCD1234EF.com.yourcompany.yourapp"],
        "defaults": {
          "caseSensitive": false,
          "percentEncoded": false
        },
        "components": [
          { "/": "/product/*" },
          { "/": "/profile/*" }
        ]
      }
    ]
  }
}

Per-component values override defaults.

hosting the file

Some platform-specific notes on serving the file with the correct Content-Type:

Nginx

location = /.well-known/apple-app-site-association {
    default_type application/json;
    add_header X-Content-Type-Options nosniff;
    try_files /.well-known/apple-app-site-association.json =404;
}

Note the try_files redirects to the .json extension on disk, but serves under the extension-less URL.

Apache (.htaccess)

<Files "apple-app-site-association">
    ForceType application/json
</Files>

Cloudflare Workers

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  if (url.pathname === '/.well-known/apple-app-site-association') {
    event.respondWith(new Response(JSON.stringify(aasa), {
      headers: { 'content-type': 'application/json; charset=utf-8' }
    }));
  }
});

Next.js / Vercel

Place a file at public/.well-known/apple-app-site-association and configure rewrites in vercel.json:

{
  "headers": [{
    "source": "/.well-known/apple-app-site-association",
    "headers": [{ "key": "Content-Type", "value": "application/json" }]
  }]
}

Apple CDN cache behavior

iOS doesn't fetch the AASA from your domain on every link tap. The flow:

  1. App is installed.
  2. iOS asks Apple's CDN: "give me the AASA for yourdomain.com."
  3. Apple's CDN fetches from your origin if not cached; otherwise serves the cached copy.
  4. iOS stores the result locally and consults it on every Universal Link tap.

Two cache layers: Apple's CDN, and the device's local cache. Both invalidate on:

  • App reinstall.
  • App update where the version string changes.
  • Manual swcutil invalidation on a development device.
  • Approximately 24 hours of natural CDN TTL.

You cannot force a refresh on a user's device. Plan AASA changes to be additive.

During development, append ?mode=developer to your Associated Domains entitlement to bypass Apple's CDN:

applinks:yourdomain.com?mode=developer

Remove this for production builds.

debugging

Check the file is reachable

curl -sI https://yourdomain.com/.well-known/apple-app-site-association

Expected HTTP/2 200, content-type: application/json. Anything else (302, 404, text/plain) breaks Universal Links.

Check the JSON is valid

curl -s https://yourdomain.com/.well-known/apple-app-site-association | python3 -m json.tool

If python3 -m json.tool errors, iOS will reject the file. Common JSON errors: trailing commas, single quotes, comments using // (only the "comment" field is supported).

Check Apple sees the file

Use Apple's AASA validator (Branch hosts a free tool that mirrors what iOS does) or swcutil on a Mac connected to a development iPhone:

sudo /usr/bin/swcutil show -d yourdomain.com

The output shows whether iOS fetched the file successfully, what was returned, and which app IDs/paths were registered.

Check the user-side state

On the device, Settings → Apps → Your App → Universal Links should show "Allow All Domains" or list the configured domains. If the user previously chose "Open in Safari" from a long-press, this setting is what controls the override.

common AASA failures

Symptom Cause Fix
Universal Link opens Safari AASA not reachable Check curl 200 OK
Universal Link opens Safari, AASA reachable Wrong Content-Type Set application/json
Universal Link opens Safari, JSON valid Wrong appID Check Team ID + bundle ID match exactly
Universal Link opens Safari, appID correct Path pattern mismatch Check components[].'/' matches URL path
Universal Link works on dev device, fails TestFlight AASA cached at Apple CDN Wait 24h, increment version string
Universal Link works first install, fails after app update AASA fetched once and not refreshed Plan additive changes; use developer mode for testing
Two apps claim the same path First-tap user picks, then sticky Resolve via app picker UI

For non-app linking where AASA doesn't apply, the no-SDK contract is at link.boo/api.

references

  • Apple Supporting Associated Domains documentation
  • Apple App Site Association format specification (iOS 13+)
  • Apple swcutil debugging tool
  • Apple Universal Links technical reference

Stop losing the click after the tap.

linkboo escapes the in-app browser so your real page loads — fast.

Start for free →