Glossary/Implementation artifacts
/.well-known/ Directory
Definition
The /.well-known/ directory is a reserved path at the root of a domain, defined by RFC 8615, where services publish machine-readable metadata files at standardised URLs.
It exists so that a client can find a domain's declaration about itself without asking anyone. Both mobile deep linking association files live there — apple-app-site-association and assetlinks.json — alongside certificate challenges, security contacts and payment verification. The directory is trivially simple and breaks constantly, always for the same handful of reasons.
What RFC 8615 actually says
The specification is short. It reserves the path prefix /.well-known/ on every host, and establishes an IANA registry of suffixes so that two services cannot claim the same filename. A well-known URI is always https://<host>/.well-known/<registered-name> — no subdirectories in the middle, no version segment, no query string.
| Path | Used by | Notes |
|---|---|---|
apple-app-site-association | iOS Universal Links, App Clips, password AutoFill | No file extension. Content-Type: application/json |
assetlinks.json | Android App Links, Credential Manager | A JSON array, not an object |
acme-challenge/<token> | Let's Encrypt and other ACME issuers | Must be reachable over plain HTTP |
security.txt | Vulnerability disclosure contacts | RFC 9116 |
change-password | Password managers | Redirects to your change-password page |
apple-developer-merchantid-domain-association | Apple Pay on the web | Also extension-less |
openid-configuration | OpenID Connect discovery | Under /.well-known/openid-configuration |
The AASA file has a legacy location too
iOS 9 accepted apple-app-site-association at the domain root as well as under /.well-known/. iOS still checks the root as a fallback, but new setups should use /.well-known/ only — and never publish two copies, because keeping them in sync is a problem nobody remembers they have.
The hosting rules that apply to all of it
Consumers of well-known URIs are machines with no user to ask, so every one of them is strict in the same ways.
| Rule | Symptom when broken |
|---|---|
| HTTPS with a valid, publicly trusted certificate | Silent fetch failure |
A 200 on the first request — no 301, no 302 | The most common cause of a broken association |
Correct Content-Type | Fetched and discarded without an error |
| No authentication, no cookie wall, no bot challenge | A WAF 403 that a browser never sees |
| No geo-blocking or rate limiting | Works from your office, fails from Apple's or Google's fetchers |
Exact filename, including the absence of .json | 404 |
Served per host — www and apex are separate | One host works, the other does not |
The redirect rule deserves emphasis: a browser follows a redirect and shows you the file, so a domain that redirects example.com to www.example.com looks perfectly healthy in a browser and fails every association check. Always test with headers visible.
# Expect exactly one 200. Any 301/302 in the chain is a failure.
curl -sSIL https://example.com/.well-known/apple-app-site-association
# Both hosts, both files
for host in example.com www.example.com; do
for f in apple-app-site-association assetlinks.json; do
echo "== $host/$f"
curl -sSI "https://$host/.well-known/$f" | head -3
done
done
# Confirm the body is valid JSON and not an HTML error page
curl -sS https://example.com/.well-known/assetlinks.json | python3 -m json.toolWhy the directory vanishes in production
The leading dot is the problem. A large amount of tooling treats dot-prefixed paths as hidden or dangerous, and does so silently. If the file is committed and 404s in production, the cause is almost always on this list.
| Layer | What happens | Fix |
|---|---|---|
| nginx | A location ~ /\. rule denies all dotfiles | Add an explicit location ^~ /.well-known/ above it |
| Apache | .htaccess blocks hidden files, or rewrites everything to the app | Exclude the path before the catch-all rewrite |
| Build and upload scripts | Globs like * skip dot-prefixed directories | Copy explicitly, or use cp -r on the parent |
Docker COPY | Same glob behaviour | COPY public/ /app/public/ rather than public/* |
| .gitignore or .dockerignore | A broad .* pattern excludes the directory | Add a negation for !.well-known/ |
| Static site frameworks | Files must be in the served directory: public/.well-known/ | Verify the built output, not the source tree |
| CDN or WAF | Bot protection returns 403 to non-browser clients | Allowlist the path |
| SPA routing | Every unknown path returns index.html with a 200 | Exclude /.well-known/ from the catch-all |
A 200 that returns HTML is worse than a 404
SPA catch-all routing returns your index.html with a 200 status for a missing file. Every naive check passes — the request succeeded — and the association fails because the body is not JSON. Check the body, not the status code.
# ^~ takes precedence over the regex location below it
location ^~ /.well-known/ {
root /var/www/example.com/public;
default_type application/json;
add_header Cache-Control "public, max-age=3600";
try_files $uri =404; # never fall through to the SPA
}
location ~ /\. {
deny all;
}Practical guidance
- Serve the directory from static hosting, not from application code — one fewer thing that can 500 at 3am.
- Put the files under version control next to the app they authorise, so a bundle ID change and a file change land in the same commit.
- Add a CI check that fetches both files from production and asserts a
200, a JSON content type, and the expected identifier in the body. - Cache for an hour, not a year. A long
max-ageon a file you occasionally need to fix is a bad trade. - When you add a host to an entitlement or manifest, add the file to that host in the same change.
If a file is confirmed missing, the ordered checklist in aasa-file-not-found covers the iOS case end to end, and how-to-validate-assetlinks-json covers Android.
Deep link debugger
Enter a domain and it fetches both well-known files the way Apple's CDN and Android's verifier do — following and reporting redirects, checking content type, and parsing the body rather than trusting the status code — then names the step that fails.
Open the deep link debugger →Frequently asked questions
- What is the /.well-known/ directory used for?
- It is a reserved path defined by RFC 8615 where a domain publishes machine-readable statements about itself at standardised URLs. Mobile deep linking uses it for apple-app-site-association and assetlinks.json; certificate authorities use it for ACME challenges; other entries cover security contacts, password-change endpoints and Apple Pay domain verification.
- Why does my .well-known file return 404 in production?
- Usually because something in the pipeline treats the leading dot as a hidden file. Common causes are an nginx location rule denying dotfiles, a build script whose glob skips dot-prefixed directories, a .gitignore or .dockerignore pattern excluding it, or a static framework that only serves files from a specific public directory. Check the deployed artifact rather than the repository.
- Can the .well-known files be served through a redirect?
- No. Apple's CDN does not follow redirects when fetching apple-app-site-association, and Android's verifier is equally strict for assetlinks.json. Because browsers do follow redirects, the file looks correct when you open it manually and fails every automated check. Test with curl -sSIL and confirm a single 200 with no 301 or 302 in the chain.
- Does apple-app-site-association have to be in /.well-known/?
- It should be. iOS still checks the domain root as a legacy fallback from iOS 9, but /.well-known/ is the documented location and the one to use for new setups. Do not publish both copies: they drift apart, and diagnosing which one a device fetched is far harder than maintaining a single file.
- Should .well-known files be served with a cache header?
- Yes, but a short one — around an hour. Apple's CDN and Android's verifier apply their own caching on top, so a long max-age mostly adds delay when you need to correct a mistake. Serving with no cache header at all is also fine and avoids the case where a stale intermediary copy outlives your fix.
Related terms
- Apple App Site Association (AASA) — The apple-app-site-association file is a JSON document hosted at a domain's /.well-known/ path that tells iOS which app is allowed to handle which URLs on that domain.
- assetlinks.json — assetlinks.json is a Digital Asset Links statement file hosted at a domain's /.well-known/ path that authorises a named Android app, identified by package name and signing certificate fingerprint, to handle that domain's URLs.
- AASA file not found — An AASA file not found error means Apple's content delivery network could not retrieve a usable apple-app-site-association file from a domain, which disables Universal Links for that domain entirely.
- Validating assetlinks.json — Validating assetlinks.json means confirming four separate things: that Android can fetch the file, that it parses as a valid statement list, that it names the fingerprint of the shipped build, and that verification passed on a device.