Universal Links
How to generate and host an apple-app-site-association file (the right way)
A complete guide to creating, hosting, and serving an AASA file for Universal Links, App Clips, Passkeys, and Handoff — including the gotchas that silently break iOS deep linking.
If you’re shipping an iOS app that needs Universal Links, App Clips, Passkeys, or Handoff, you need an apple-app-site-association file — a small JSON document hosted on your domain that tells iOS which app to open for which URLs.
It sounds simple. The file itself is maybe 20 lines of JSON. But Apple is uncompromising about how it must be served, and a single misconfigured header silently breaks deep linking with no error message anywhere — links just open Safari instead of your app.
This guide walks through the right way to generate the file, the half-dozen ways hosting can go wrong, and how to verify everything works before submitting your app.
What an AASA file actually does
When a user taps a link to your domain on an iOS device, Apple’s swcd daemon (the “shared web credentials” daemon) checks an apple-app-site-association file hosted at your domain. If the file says the URL pattern matches one your app handles, iOS opens your app instead of Safari.
The same file declares four separate capabilities:
| Capability | Key in the AASA file | What it enables |
|---|---|---|
| Universal Links | applinks |
Tap a web link → open in your app |
| App Clips | appclips |
Invoke a lightweight version of your app from NFC, QR, or links |
| Passkeys / Password AutoFill | webcredentials |
Share saved credentials between your app and website |
| Handoff | activitycontinuation |
Continue an activity across devices |
All four ride on the same file at the same path — you only need to host one.
Where the file goes
Apple looks for the file at:
https://your-domain.com/.well-known/apple-app-site-association
Some legacy Apple documentation and most third-party validators (Branch’s checker, getuniversal.link) also check the legacy root path:
https://your-domain.com/apple-app-site-association
Both should return the same file with the same headers. Modern iOS uses /.well-known/, but serving both removes a class of false-positive failures in debugging tools.
What the file must contain
A minimal file for Universal Links looks like this:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.yourapp"],
"components": [
{ "/": "/products/*", "comment": "Product pages" },
{ "/": "/admin/*", "exclude": true }
]
}
]
}
}
A few notes from the spec:
appIDs(plural, with a capital “I” in IDs) is the modern field. Older guides may showappID(singular) inside anappsarray — that’s the legacy format and doesn’t take advantage of multiple bundle IDs.- The Team ID is the 10-character alphanumeric prefix from your Apple Developer account.
- Path patterns support
*(any characters),?(any single character), andNOTfor exclusions. They are case-sensitive. - Query strings and fragment identifiers are ignored when matching paths.
- An empty
appsarray underapplinksis not required in the modern format, despite what some older documentation suggests.
If you also want App Clips, Passkeys, and Handoff:
{
"applinks": { "details": [ /* ... */ ] },
"appclips": { "apps": ["ABCDE12345.com.yourapp.Clip"] },
"webcredentials": { "apps": ["ABCDE12345.com.yourapp"] },
"activitycontinuation": { "apps": ["ABCDE12345.com.yourapp"] }
}
The serving requirements that silently break things
This is where most setups fail. Apple’s swcd is strict:
| Requirement | Why it bites |
|---|---|
| HTTPS only | An HTTP-only domain is invisible to Apple. |
| TLS 1.2 or higher | Older TLS configs fail without an error. |
| No redirects (no 301/302) | Apple won’t follow them. The file must be at the canonical URL with a 200 response. |
Content-Type: application/json |
The wrong MIME type causes a silent parse failure. |
No .json extension on the filename |
Filename is literally apple-app-site-association — no extension. |
| Public, no auth | If the file is gated behind a login or IP allowlist, Apple’s CDN can’t fetch it. |
| 128 KB max file size | Apple’s swcd rejects larger files. With sensible app/path counts you’ll never hit this, but keep an eye on it if you generate the file dynamically. |
AASA-Bot/1.0.0 user agent |
Cloudflare’s bot challenge can block it. If you front your origin with Cloudflare, allowlist the bot. |
The iOS 14+ CDN behavior
Starting in iOS 14, devices don’t fetch your AASA file directly. Apple’s CDN does it on behalf of every device:
https://app-site-association.cdn-apple.com/a/v1/your-domain.com
The CDN refreshes roughly every 24–48 hours, so a freshly-deployed change can take a while to propagate. To inspect what the CDN currently has for your domain, hit that URL directly in a browser.
If you need to bypass the CDN during development — e.g., you’re iterating on path patterns and want changes to land instantly — change your Xcode Associated Domains entitlement from:
applinks:your-domain.com
to:
applinks:your-domain.com?mode=developer
That tells iOS to fetch the AASA file directly from your origin, no CDN involved. Remove the ?mode=developer flag before submitting to the App Store — it’s a development-only mode.
iOS does not re-scrape the AASA file on app updates from the App Store (a known limitation). To force a refresh during development, delete and reinstall the app. Long-press a link in the Notes app to verify whether iOS picked up your latest config.
Subdomains are separate domains
Apple treats myapp.com and www.myapp.com as distinct domains for AASA purposes. Each needs its own AASA file at its own domain root, and each needs its own line in your Xcode Associated Domains capability:
applinks:myapp.com
applinks:www.myapp.com
If you only want one canonical version, redirect the other at your DNS provider — most registrars support apex-to-www (or the reverse) redirects natively.
Three ways to actually host the file
1. Static hosting (GitHub Pages, S3, Firebase Hosting)
Cheap and fine for a single domain. The catch: you have to manage Content-Type headers manually (GitHub Pages can’t set them per-file; you’ll need a custom workflow), and you can’t easily share the same domain with marketing content.
2. Edit your nginx/Caddy/Apache config
If you already run a web server on your domain, add a location block that serves the file with the correct headers and no redirects. Easy if you’re a sysadmin; surprisingly fiddly if you’re not.
3. Use OrbitKit
This is what we built. Configure your App IDs and paths in a dashboard, point your DNS, and OrbitKit serves the file at both /.well-known/apple-app-site-association and /apple-app-site-association with the right Content-Type, no redirects, automatic HTTPS, no Cloudflare bot-challenge problems, and a 128 KB size guard. Free tier covers a single app.
The right answer depends on whether you already have web infrastructure for the domain. If you do and you’re comfortable editing config files, options 1 or 2 are fine. If you just want it to work on the first try, that’s our pitch.
How to verify it actually works
- HTTP check with curl:
curl -I https://your-domain.com/.well-known/apple-app-site-associationYou want
HTTP/2 200,content-type: application/json, and no redirects. - JSON validity:
curl -s https://your-domain.com/.well-known/apple-app-site-association | jq .If
jqerrors, your JSON is broken. -
Apple’s CDN view: Visit
https://app-site-association.cdn-apple.com/a/v1/your-domain.comand confirm Apple has the same content. -
Branch’s free validator: branch.io/resources/aasa-validator — checks both paths, validates JSON, flags the common gotchas.
- On-device test: Send yourself a link via Notes or iMessage. Long-press it. You should see “Open in <App Name>” as the first option.
Common failure modes and what to check
| Symptom | Likely cause |
|---|---|
| Link opens Safari instead of app | AASA missing, wrong Content-Type, behind a redirect, or app entitlement mismatch |
apple-app-site-association returns 404 |
File at wrong path, or behind app routing that doesn’t serve static files |
| Works in dev, breaks in prod | Different domain or subdomain (each needs its own file), or Cloudflare bot-mode blocking AASA-Bot |
| Works for a while, then stops | App was updated via App Store but iOS didn’t re-fetch — delete and reinstall |
| Path matches local but not online | Apple’s CDN cached an older version; hit the CDN URL to compare |
| Apple Developer Forums shows successful AASA fetch but app still doesn’t open | App entitlement missing or wrong Bundle ID — re-check the applinks: line in Associated Domains |
Further reading
- Apple — Supporting Universal Links in Your App
- Apple — TN3155: Debugging universal links
- OrbitKit Universal Links docs — how OrbitKit configures the file
- OrbitKit Custom Domain docs — DNS + SSL setup for AASA hosting
- OrbitKit App Clips docs
- OrbitKit Passkeys / WebCredentials docs
- OrbitKit Handoff docs