Server-Side Preparation for Deep Linking
Deep linking is usually explained through routing code on the mobile side. That part matters, but in the world of verified links there is another quiet, critical piece: the server-side preparation.
iOS Universal Links and Android App Links want to establish a secure association between a domain and an app. That association is not verified from inside the app — it is verified through small JSON files published under the domain. If the file sits at the wrong path, gets redirected, has a wrong MIME type, or is not publicly reachable, the link may never make it into the app, no matter how correct your mobile code is.
This post focuses only on the server-side work:
apple-app-site-associationfor iOSassetlinks.jsonfor Android- The correct
.well-knownpath - MIME type, redirect, and access checks
- Quick verification commands to run after deploy
The examples use placeholder values like example.com and com.example.app. You should substitute your own domain, bundle id, package name, and signing details.
Why we publish files on the server
The core idea of verified deep linking is simple: "Does this domain actually grant this app permission to open its links?"
On iOS, that verification is done through the apple-app-site-association file. On Android, the equivalent is assetlinks.json. The operating system downloads these files from a known location under the domain, compares their contents with the app's own identity, and — if everything matches — can route links directly into the app.
This is why server preparation deserves more than a "just upload a JSON file" mindset. A small HTTP detail here can silently break the entire deep linking flow.
General folder layout
For both platforms, the files go under the .well-known directory at the root of the domain:
https://example.com/.well-known/apple-app-site-association
https://example.com/.well-known/assetlinks.json
A few rules apply:
- The
.well-knowndirectory must be publicly accessible. - The files must not sit behind authentication.
- The endpoints must return
200 OK. - There must be no
301or302redirects. - The response body must be JSON, not HTML.
- CDN, WAF, bot protection, or geo-blocking must not interfere with these files.
If the app should open links for both example.com and www.example.com, serve the files on both hosts at the correct path. Relying on a redirect from the root domain to www is not a reliable strategy for verification.
iOS: apple-app-site-association
The file name on the iOS side must be exactly:
apple-app-site-association
The file must not have a .json extension. Locally, or while preparing the file, you can keep it as apple-app-site-association.json, but by the time it reaches the server the extension must be dropped.
The file must be reachable at this exact URL:
https://example.com/.well-known/apple-app-site-association
An example payload:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.app",
"paths": [
"*"
]
}
]
}
}
Here appID is the combination of the Apple Team ID (or App ID Prefix) and the iOS bundle identifier:
<TeamID>.<BundleID>
In the example:
ABCDE12345.com.example.app
The paths field specifies which URL paths the app is allowed to open. ["*"] covers everything. For a tighter setup, it is often cleaner to enumerate only the paths you actually need:
"paths": [
"/articles/*",
"/news/*"
]
Apple's more recent documentation uses the appIDs and components format. The appID/paths format shown above is the legacy format and is still common in practice. The important thing is to not mix the two formats within the same file.
Server configuration for AASA
Because the AASA file has no extension, some web servers will serve it as application/octet-stream or try to force it as a download. The behavior we actually want:
- Opening the URL in a browser shows the file.
- The file is not forced as a download.
Content-Typeisapplication/json.- The response is
200 OK. - No redirects.
Nginx example:
location = /.well-known/apple-app-site-association {
default_type application/json;
try_files $uri =404;
}
For Apache, using ForceType on the extensionless AASA file is the safer choice:
<Files "apple-app-site-association">
ForceType application/json
</Files>
If your hosting layer exposes metadata — for example S3 or a similar object storage — set the Content-Type metadata on the file to application/json. And if you have an SPA fallback rule, make sure this path does not fall through to index.html.
Android: assetlinks.json
On Android, the file is served with its extension:
assetlinks.json
Exact URL:
https://example.com/.well-known/assetlinks.json
An example payload:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]
The fields:
relation: declares that the app is allowed to handle URLs for this domain.namespace:android_appfor Android app association.package_name: the Android application's package name.sha256_cert_fingerprints: the SHA-256 fingerprint of the certificate that signs the app.
The SHA-256 fingerprint is easy to get wrong. If you use Play App Signing, you usually need the app signing certificate fingerprint shown in the Play Console — not the upload key. Debug builds have their own debug keystore fingerprint, which is different again. Rather than mixing test certificates into your production domain, it is cleaner to use a dedicated flavor or a separate test domain.
Server configuration for assetlinks.json
Because the Android file has a .json extension, most servers will serve it with the right MIME type out of the box. Still, the same checks apply as on iOS:
Content-Typeisapplication/json.- The file is reachable over HTTPS.
- No redirects.
- No authentication.
- The JSON is valid.
For Apache, if you want to make the .json MIME type explicit:
AddType application/json .json
For Nginx, the default mime.types usually covers JSON. If you prefer a dedicated location block:
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
}
Post-deploy verification
Once the files are uploaded, the first check can be in the browser — but the real check is inspecting the HTTP response from the terminal.
iOS:
curl -i https://example.com/.well-known/apple-app-site-association
Android:
curl -i https://example.com/.well-known/assetlinks.json
What you want to see:
HTTP/2 200
content-type: application/json
What you do not want to see:
HTTP/2 301
HTTP/2 302
content-type: text/html
content-disposition: attachment
To additionally confirm the JSON is valid:
curl -fsS https://example.com/.well-known/assetlinks.json | jq .
curl -fsS https://example.com/.well-known/apple-app-site-association | jq .
Note that I deliberately do not use -L. If there is a redirect, I want to catch it, not hide it.
Most common mistakes
Most of the problems in this setup show up in the server response before they ever reach the app code:
- Uploading the file as
apple-app-site-association.jsoninstead of the extensionless name. - Serving the AASA file from somewhere other than
.well-known. - Letting
/.well-known/*fall through to an SPAindex.html. - Redirecting HTTP to HTTPS, or the root domain to
www, on these endpoints. - Putting the file behind basic auth, VPN, WAF, or bot protection.
- Using the wrong SHA-256 fingerprint on Android.
- Using the wrong Team ID or bundle identifier on iOS.
- Mixing comments, trailing commas, or an HTML body into the JSON.
- Expecting changes to propagate instantly without clearing CDN caches.
Caching is another practical detail. On Android, the device may need a re-verification trigger. On iOS, Associated Domains verification goes through Apple's CDN, so do not expect updates to hit every device immediately.
Short release checklist
Before shipping, it is worth walking through this list once:
- Is
https://example.com/.well-known/apple-app-site-associationreachable? - Is the AASA file extensionless?
- Is
https://example.com/.well-known/assetlinks.jsonreachable? - Do both endpoints return
200 OK? - Do both endpoints return
application/json? - Are there any redirects?
- Are the files reachable publicly, without authentication?
- Is the iOS
appIDbuilt from the correct Team ID and Bundle ID? - Is the Android
package_namecorrect? - Does the Android SHA-256 fingerprint match the release signing certificate?
- Are the files published for all variants of the domain?
No matter how carefully the mobile side of deep linking is written, verified link behavior is not reliable until this checklist is done. The good news: once the server side is set up correctly, most of the uncertainty in deep link debugging disappears.
