Universal Links
Set up Universal Links AASA hosting without nginx (or any web server)
iOS Universal Links don't actually need nginx, Apache, or any traditional web server. Five real alternatives for hosting the AASA file — and the gotcha each one hides.
You’re shipping an iOS app, you need Universal Links, and Apple’s documentation tells you to host an apple-app-site-association (AASA) file on your domain. The first thing every search result for “Universal Links setup” suggests is configuring nginx — Content-Type: application/json, no redirects, no extension on the path, the whole production-server checklist.
You don’t need nginx. You don’t need any traditional web server. You don’t need to maintain Linux, renew SSL certs manually, or learn Cloudflare’s nginx-equivalent config syntax. There are five real ways to host an AASA file that work for iOS Universal Links, App Clips, Passkeys, and Handoff — none of them require running nginx.
This post is the five options ranked by setup time, the iOS-specific gotcha each one hides, and how to verify the file is being served the way iOS’s swcd daemon expects.
If you want the broader picture of how AASA fits into the App Store submission, see every URL and file Apple requires for an iOS App Store submission. For an end-to-end walkthrough including DNS and verification, see how to generate and host an apple-app-site-association file.
Why “you need nginx” is everywhere — and why it’s wrong
The reason every tutorial mentions nginx is historical: in 2015, when Universal Links shipped with iOS 9, the canonical example in Apple’s developer guide assumed you ran your own web infrastructure, because the indie-iOS-on-Cloudflare-Pages combination didn’t exist yet. The four iOS-specific requirements for serving the AASA file were always about the response (Content-Type, no redirects, HTTPS, no extension), never about the server software.
Any web stack that lets you control these four things will host an AASA file correctly:
- The file is served at exactly
https://yourdomain/.well-known/apple-app-site-association(no extension, no redirect). - The response has
Content-Type: application/json(orapplication/pkcs7-mimefor signed AASA, but you almost certainly don’t need that). - The TLS certificate is valid and trusted (no self-signed, no expired).
- The file is under 128 KB (Apple’s
swcddaemon refuses larger files).
Anything that meets those four constraints works. nginx is one option. So is GitHub Pages, Cloudflare Pages, Netlify, Vercel, AWS S3 + CloudFront, Firebase Hosting, and a handful of services purpose-built for hosting Apple-required files.
Option 1: GitHub Pages (free, ~10 minutes)
GitHub Pages serves your AASA file with HTTPS by default and supports custom domains. The setup:
- Create a repo (or use an existing one).
- Add the AASA file at
.well-known/apple-app-site-association(no extension). - Enable GitHub Pages in the repo settings, choose a branch (usually
main). - Add a
CNAMEfile with your custom domain. - Configure DNS to point to GitHub Pages (
yourdomain.com → 185.199.108.153or use ALIAS for an apex).
The iOS gotcha: GitHub Pages serves files with Content-Type based on the file extension. An AASA file has no extension, which means GitHub Pages serves it as text/plain or application/octet-stream depending on the day. iOS rejects it.
The workaround is to add a _headers file at your repo root with:
/.well-known/apple-app-site-association
Content-Type: application/json
GitHub Pages doesn’t natively support _headers (that’s a Netlify/Cloudflare convention). For GitHub Pages specifically, the workaround is to wrap the AASA file in a Jekyll layout that sets the content type via meta tag — which doesn’t actually change the HTTP response header. GitHub Pages cannot reliably serve the right Content-Type for AASA without extension. This is a real, well-documented limitation.
Result: GitHub Pages works for marketing sites and docs, but not for AASA hosting. Move on.
Option 2: Firebase Hosting (free tier, ~15 minutes)
Firebase Hosting actually solves the Content-Type problem out of the box if you configure firebase.json correctly:
{
"hosting": {
"public": "public",
"headers": [
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{ "key": "Content-Type", "value": "application/json" }
]
}
]
}
}
Setup:
npm install -g firebase-toolsfirebase init hosting- Drop the AASA file at
public/.well-known/apple-app-site-association - Add the headers config above
firebase deploy
The iOS gotcha: Firebase’s free Spark plan has a 360 MB/day egress limit. For an AASA file that’s a few KB and gets fetched by iOS once per app install, that’s fine. If your domain ever gets a traffic spike (a viral link, a search bot crawling your AASA endpoint excessively), you can blow through the quota and the file becomes intermittently unreachable — which iOS handles by silently breaking Universal Links.
Result: Works correctly. Adequate for indie apps. Watch the egress quota if you scale.
Option 3: Cloudflare Pages (free, ~10 minutes)
Cloudflare Pages is GitHub Pages with the Content-Type problem fixed. The _headers convention is supported natively:
- Push a repo to GitHub/GitLab.
- Connect it to Cloudflare Pages.
- Add
_headersat repo root:
/.well-known/apple-app-site-association
Content-Type: application/json
- Add the AASA file at
.well-known/apple-app-site-association.
The iOS gotcha: Cloudflare’s free tier serves the file from their global edge — but their edge cache TTL defaults to whatever the origin sends, and “no Cache-Control header” means short TTLs at the edge. AASA changes are infrequent, so you want a long TTL, but iOS also caches AASA files for ~24 hours per device. The interaction means a change you push might take ~36 hours to fully propagate to all devices. Plan deploys accordingly — don’t ship a Universal Links config change the day of an App Store release.
Result: Works correctly with the right _headers. Most reliable free option.
Option 4: Netlify or Vercel (free, ~10 minutes)
Same as Cloudflare Pages — modern static hosts with header customization.
Netlify: Use the same _headers syntax as Cloudflare.
Vercel: Use vercel.json:
{
"headers": [
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{ "key": "Content-Type", "value": "application/json" }
]
}
]
}
The iOS gotcha: Both have free-tier bandwidth limits (Netlify 100 GB/month, Vercel 100 GB/month) — these are huge for AASA hosting alone but if you also host a marketing site on the same project, the bandwidth gets shared. For AASA-only deployment, you’ll never hit the limit.
Result: Works correctly. Pick whichever you already use for other things.
Option 5: A purpose-built service (free or paid, 5 minutes)
A few services exist specifically to host Apple-required files for iOS developers — AASA, privacy policy URL, support URL, the Sign in with Apple verification file, the Apple Pay merchant domain file. These are usually faster to set up than configuring static hosting because the AASA-specific gotchas (Content-Type, no extension, JSON validity) are handled by default. They typically include:
- A web UI for configuring App IDs and path patterns instead of hand-writing JSON.
- Built-in JSON validation against the AASA schema.
- Automatic SSL on custom domains.
- Hosting for the other Apple-required files in the same place.
OrbitKit is one such service ($5/month per app). Others exist; the trade-off is paying $5/month vs. spending an evening configuring Cloudflare Pages and remembering to renew the DNS records every two years.
Verification: how to check the AASA file is actually working
Whichever hosting option you pick, verify the response with curl:
curl -I https://yourdomain.com/.well-known/apple-app-site-association
You’re looking for:
HTTP/2 200(orHTTP/1.1 200 OK).Content-Type: application/json(exactly).- No
Location:header (no redirects). Content-Lengthunder 128 KB.
If the response is Content-Type: text/plain or has a redirect, iOS will reject it silently — your Universal Links open in Safari instead of the app, with no error to debug.
For end-to-end testing on a real device, see our Universal Links debugging checklist.
What about the other Apple files? (SiwA, Apple Pay, AASA-adjacent)
Universal Links isn’t the only thing that requires hosting verification files at /.well-known/. If you also use:
- Sign in with Apple on the web — needs
apple-developer-domain-association.txtat/.well-known/. - Apple Pay on the web — needs
apple-developer-merchantid-domain-association(no extension) at/.well-known/. - Apple Wallet pass type domain association — needs
apple-wallet-order-type-association(no extension) at/.well-known/.
Each has its own MIME type quirks. Cloudflare Pages, Netlify, and Vercel handle all of them via _headers / config files. Firebase Hosting handles them via firebase.json. GitHub Pages still doesn’t handle any of them correctly (same extension-less Content-Type problem as AASA).
The full list of Apple-required public files is in the App Store submission checklist post.
Summary: pick the option that matches your setup
| Hosting option | Setup time | Free? | AASA Content-Type | Other Apple files |
|---|---|---|---|---|
| nginx (DIY) | 1-2 days | depends | manual config | manual config |
| GitHub Pages | 10 min | yes | ✗ broken | ✗ broken |
| Firebase Hosting | 15 min | yes (limits) | ✓ via firebase.json | ✓ same |
| Cloudflare Pages | 10 min | yes | ✓ via _headers | ✓ same |
| Netlify / Vercel | 10 min | yes (limits) | ✓ via _headers / vercel.json | ✓ same |
| OrbitKit (purpose-built) | 5 min | $5/mo | ✓ default | ✓ default + privacy policy / support / deletion |
If you’re an Apple-platform developer who’s never run a web server and doesn’t want to learn nginx config to ship Universal Links: Cloudflare Pages is the best free option, OrbitKit is the fastest paid option that also covers the other Apple-required URLs in the same place. Either way, you don’t need nginx.
For the broader App Store web-infrastructure picture, see every URL and file Apple requires for iOS and macOS App Store submission. For the deep dive on AASA file format and gotchas, see how to generate and host an apple-app-site-association file.