iOS Universal Links
Associated domains, apple-app-site-association, and CDN hosting - the iOS cookbook for opening https:// marketing URLs directly in your Expo SDK 57 app without a custom scheme disambiguation sheet.
Search across all documentation pages
Associated domains, apple-app-site-association, and CDN hosting - the iOS cookbook for opening https:// marketing URLs directly in your Expo SDK 57 app without a custom scheme disambiguation sheet.
Quick-reference recipe card - copy-paste ready.
// app.config.ts
export default {
expo: {
scheme: "shopapp",
ios: {
bundleIdentifier: "com.example.shopapp",
associatedDomains: ["applinks:shop.example.com"],
},
},
};// https://shop.example.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.shopapp",
"paths": ["/orders/*", "/product/*", "/invite/*"]
}
]
}
}When to reach for this:
https:// links that should open the app when installed.myapp:// prompts.End-to-end setup for https://shop.example.com/orders/* on an Expo prebuild app.
Step 1 - Entitlements via app.config.ts
// app.config.ts
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "ShopApp",
slug: "shop-app",
scheme: "shopapp",
ios: {
bundleIdentifier: "com.example.shopapp",
associatedDomains: ["applinks:shop.example.com"],
},
android: {
package: "com.example.shopapp",
},
};
export default config;Step 2 - Host AASA on your CDN
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.shopapp",
"paths": [
"/orders/*",
"/product/*",
"NOT /admin/*",
"NOT /api/*"
]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.shopapp"]
}
}Serve at both paths (Apple checks either):
https://shop.example.com/apple-app-site-associationhttps://shop.example.com/.well-known/apple-app-site-associationStep 3 - CDN response headers
Content-Type: application/json
Cache-Control: max-age=3600.json extension required; raw JSON body.ABCDE12345 with your Apple Team ID from the Developer portal.Step 4 - Expo Router screen
// app/orders/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native";
export default function OrderScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={{ padding: 16 }}>
<Text>Order {id}</Text>
</View>
);
}Step 5 - Build and verify
npx expo prebuild --platform ios
npx expo run:ios
# Simulator: open universal link
xcrun simctl openurl booted "https://shop.example.com/orders/42"What this demonstrates:
/orders/42.applinks: domain in your entitlements.https:// link, iOS opens your app and passes the URL to React Native.| Pattern | Matches |
|---|---|
/orders/* | /orders/42, /orders/42/receipt |
/product/* | /product/widget |
NOT /admin/* | Excludes admin URLs even if parent would match |
* | Everything on the domain - avoid unless app owns entire host |
| Check | Pass criteria |
|---|---|
| TLS | Valid certificate; no mixed http:// |
| Redirects | AASA URL returns 200 directly |
| Content-Type | application/json or application/pkcs7-mime (signed) |
| Size | Under 128 KB |
| Accessibility | Public internet - no VPN or IP allowlist |
# Validate AASA with Apple's CDN (device will eventually see same data)
curl -s "https://app-site-association.cdn-apple.com/a/v1/shop.example.com" | jq .appID exactly.TEAMID.bundleIdentifier from Apple Developer → Membership.www → apex redirect breaks fetch. Fix: serve AASA on the exact host in associatedDomains.eas build --platform ios after config change./api/* in paths - API JSON responses should not open the app. Fix: explicit NOT rules or narrow prefixes.SFSafariViewController or <a target="_blank"> for external open.| Alternative | Use When | Don't Use When |
|---|---|---|
| Custom scheme only | Internal tools, OAuth redirects | Consumer email campaigns |
| Firebase Dynamic Links (deprecated) | Legacy projects already integrated | Greenfield - Google shut down Aug 2025 |
| Branch.io / AppsFlyer OneLink | Deferred deep links + attribution | Simple verified links suffice |
| Smart App Banner only | Web-to-app nudge without full universal links | You need tap-to-open without Safari |
TEAMID.com.example.shopapp - no spaces.details entries with different appID values for dev and prod bundle IDs.applinks:staging.example.com vs applinks:shop.example.com.curl -I for redirects and content type.applinks:*.example.com in entitlements and separate details per subdomain in AASA.*.example.com in paths is not valid - list each subdomain explicitly./orders/42?ref=email works if /orders/* matches; query does not affect path rules.details entries - iOS picks the installed app matching appID.https://shop.example.com/orders/42 to app/orders/[id].tsx automatically when scheme/host are configured.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026