Deeplinkly

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.

Files commonly published under /.well-known/.
PathUsed byNotes
apple-app-site-associationiOS Universal Links, App Clips, password AutoFillNo file extension. Content-Type: application/json
assetlinks.jsonAndroid App Links, Credential ManagerA JSON array, not an object
acme-challenge/<token>Let's Encrypt and other ACME issuersMust be reachable over plain HTTP
security.txtVulnerability disclosure contactsRFC 9116
change-passwordPassword managersRedirects to your change-password page
apple-developer-merchantid-domain-associationApple Pay on the webAlso extension-less
openid-configurationOpenID Connect discoveryUnder /.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.

Serving rules and the symptom when each is violated.
RuleSymptom when broken
HTTPS with a valid, publicly trusted certificateSilent fetch failure
A 200 on the first request — no 301, no 302The most common cause of a broken association
Correct Content-TypeFetched and discarded without an error
No authentication, no cookie wall, no bot challengeA WAF 403 that a browser never sees
No geo-blocking or rate limitingWorks from your office, fails from Apple's or Google's fetchers
Exact filename, including the absence of .json404
Served per host — www and apex are separateOne 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.

Check the way a machine does
# 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.tool

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

Common ways /.well-known/ is dropped between the repository and the server.
LayerWhat happensFix
nginxA location ~ /\. rule denies all dotfilesAdd an explicit location ^~ /.well-known/ above it
Apache.htaccess blocks hidden files, or rewrites everything to the appExclude the path before the catch-all rewrite
Build and upload scriptsGlobs like * skip dot-prefixed directoriesCopy explicitly, or use cp -r on the parent
Docker COPYSame glob behaviourCOPY public/ /app/public/ rather than public/*
.gitignore or .dockerignoreA broad .* pattern excludes the directoryAdd a negation for !.well-known/
Static site frameworksFiles must be in the served directory: public/.well-known/Verify the built output, not the source tree
CDN or WAFBot protection returns 403 to non-browser clientsAllowlist the path
SPA routingEvery unknown path returns index.html with a 200Exclude /.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.

nginx: serve /.well-known/ before the dotfile deny rule
# ^~ 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-age on 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.jsonassetlinks.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 foundAn 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.jsonValidating 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.