Deep linking lets a URL open a specific screen inside your mobile app — powering push notification taps, marketing campaigns, password-reset flows, and social sharing. Without it, links either fail silently or drop users on a generic home screen.
Table of Contents
Getting deep links right in Expo requires understanding three distinct mechanisms: the `scheme` field in app.json for custom-URL linking, iOS Universal Links using Apple’s AASA verification file, and Android App Links using Google’s Digital Asset Links. Choosing the wrong one for your use case is the most common source of wasted effort. This guide covers all three with exact configuration, code, and testing commands.
Quick Answer
To enable deep linking in Expo, add `”scheme”: “myapp”` inside the `expo` object in app.json — this makes your app respond to `myapp://` URLs. For HTTPS-based linking (recommended for production), also add `associatedDomains` under `expo.ios` and `intentFilters` under `expo.android`, then host verification files at `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json` on your domain. Always rebuild with `npx expo prebuild –clean` after any config change — the scheme is baked into the native binary and cannot be hot-reloaded.
The scheme Field in app.json Explained
The `scheme` field is the entry point for all custom-URL deep linking in Expo. Add it directly inside the `expo` object in app.json: `{ “expo”: { “scheme”: “myapp” } }`. Once the app is built and installed, any URL in the format `myapp://` will launch your app and pass the path and query string to your navigation layer.
You can also set platform-specific values with `expo.ios.scheme` or `expo.android.scheme` — these take precedence over the top-level `scheme` field. If you omit the field entirely, Expo Prebuild automatically falls back to `ios.bundleIdentifier` and `android.package` as default schemes, so the app will accept links like `com.yourcompany.myapp://` without any explicit configuration.
One key constraint: the `scheme` field is compiled into the native binary at build time. It is not available in Expo Go, which uses the fixed `exp://` scheme for every app. After changing the scheme field you must run `npx expo prebuild –clean` and rebuild — either locally with `npx expo run:ios` or `npx expo run:android`, or in the cloud with `npx eas build`.
Step 1 — Add a Custom URL Scheme
Open `app.json` and add `”scheme”: “myapp”` inside the `expo` block. If you are using Expo Router, also confirm that `”expo-router”` appears in the `plugins` array. Save the file, then run `npx expo prebuild –clean` to regenerate the native project, followed by `npx expo run:ios` or `npx expo run:android`.
To test on an iOS simulator: `xcrun simctl openurl booted “myapp:///products/42″`. On Android: `adb shell am start -a android.intent.action.VIEW -d “myapp:///products/42” com.yourcompany.myapp`. You can also use the Expo CLI helper across both platforms: `npx uri-scheme open myapp://products/42 –ios` or `–android`.
To handle incoming URLs in code without Expo Router, use `Linking.useLinkingURL()` — available since Expo SDK 55, this hook covers both cold-start launches and links received while the app is already running. The older `Linking.getInitialURL()` plus separate `Linking.addEventListener` pattern still works but `useLinkingURL` is preferred. If you are using Expo Router you can skip all manual handling; routing is automatic.
Step 2 — Configure iOS Universal Links
Universal Links use standard HTTPS URLs rather than custom schemes, so they work in iMessage, email clients, and any app that opens links. If the app is not installed, the URL falls back to your website automatically — making them the right choice for marketing campaigns, transactional emails, and any link you want to survive app uninstalls.
In app.json, add `associatedDomains` inside `expo.ios`: `”associatedDomains”: [“applinks:yourapp.com”, “applinks:www.yourapp.com”]`. Do not include `https://` — Apple requires the bare domain only. Build with `npx eas build –platform ios`; EAS automatically registers the Associated Domains entitlement with Apple so you do not need to edit entitlement files manually.
Next, create the Apple App Site Association (AASA) file. For Expo Router projects, place it at `public/.well-known/apple-app-site-association`. For legacy webpack projects use `web/.well-known/apple-app-site-association`. The file must be valid JSON with no `.json` extension, served over HTTPS with `Content-Type: application/json`. Using the modern components format (iOS 13+): `{ “applinks”: { “details”: [{ “appIDs”: [“TEAMID.com.yourcompany.myapp”], “components”: [{ “/”: “/*”, “comment”: “Match all paths” }] }] } }`. Replace `TEAMID` with your 10-character Apple Developer Team ID found in the Apple Developer portal. The AASA file must be under 128 KB uncompressed. Apple’s CDN caches the file and refreshes it approximately once per week, so changes to path patterns take time to propagate to existing installs.
Verify the file is reachable with `curl -I https://yourapp.com/.well-known/apple-app-site-association`. The Branch AASA validator (branch.io/resources/aasa-validator) is also useful for catching format errors. One critical quirk: tapping a Universal Link in Safari while the user is already on the same domain does not trigger the app — iOS only activates Universal Links when navigating from a different domain (another site, iMessage, email, etc.). Always test Universal Links on a physical device rather than a simulator, and send the test link from iMessage or another app.
Step 3 — Configure Android App Links
Android App Links use Google’s Digital Asset Links protocol for the equivalent HTTPS-based behavior. In app.json, add an `intentFilters` array under `expo.android` with `action` set to `VIEW`, `autoVerify` set to `true`, `category` set to `[“BROWSABLE”, “DEFAULT”]`, and `data` set to `{ “scheme”: “https”, “host”: “yourapp.com”, “pathPrefix”: “/” }`. The `autoVerify: true` flag is required — without it Android shows a disambiguation dialog instead of opening the app directly.
Create `public/.well-known/assetlinks.json` and host it over HTTPS with `Content-Type: application/json`: `[{ “relation”: [“delegate_permission/common.handle_all_urls”], “target”: { “namespace”: “android_app”, “package_name”: “com.yourcompany.myapp”, “sha256_cert_fingerprints”: [“AA:BB:CC:…”] } }]`. Get your SHA-256 fingerprint by running `eas credentials -p android` and selecting your build profile. If you use Google Play App Signing, include both the upload key fingerprint and the Google-managed production key fingerprint in the array — including only the upload key is a very common mistake that causes App Links to fail for users who installed from the Play Store.
After configuring and rebuilding, install the app on an Android device. Verification can take up to 20 seconds after installation. Test with `adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d “https://yourapp.com/”`. If the app opens directly with no chooser dialog, verification succeeded.
Testing Deep Links in Expo
For custom URL schemes, `npx uri-scheme open myapp://path –ios` and `npx uri-scheme open myapp://path –android` work on both real devices and simulators. The raw platform commands also work: `xcrun simctl openurl booted “myapp://path”` on iOS and `adb shell am start -a android.intent.action.VIEW -d “myapp://path”` on Android.
For Universal Links and App Links during development, run `EXPO_TUNNEL_SUBDOMAIN=mysubdomain npx expo start –tunnel` to get a stable HTTPS Ngrok URL. Update your AASA domain to `mysubdomain.ngrok.io` and the `associatedDomains` entry accordingly. You still need a physical iOS device for Universal Links — and remember to send the test link from iMessage or another app, not from Safari on the same domain. Android App Link verification requires the HTTPS domain to respond with the correct `assetlinks.json` before Android will skip the disambiguation dialog.
Common Mistakes
Expo Go does not support custom URL schemes and only handles `exp://` links. You need a development build made with `npx expo run:ios` or EAS Build to test any custom scheme. If your links open a browser or do nothing, this is the most likely cause.
Firebase Dynamic Links was shut down in August 2025. If your app still references it, migrate to an alternative service (Branch, Adjust, AppsFlyer) or build your own redirect logic using the custom URL scheme and server-side session storage to preserve the deep link destination through the install flow.
On iOS, a mismatch between the Team ID or bundle identifier in your AASA file and what is registered in Apple’s developer portal silently breaks Universal Links. Because Apple caches the AASA for up to a week, errors discovered after deployment are slow to fix.
On Android, always include the Google Play App Signing key fingerprint in `assetlinks.json` alongside your upload key fingerprint. Omitting it means App Links work in your local debug build but fail for every user who installed from the Play Store.
expo deep linking setup FAQs
What does the scheme field do in Expo’s app.json?
The `scheme` field inside the `expo` object of app.json defines the custom URL prefix your app responds to. Setting `”scheme”: “myapp”` means your app opens whenever a device receives a `myapp://` URL. The value is compiled into the native binary at build time, so you must run `npx expo prebuild –clean` and rebuild after any change. Platform-specific keys `expo.ios.scheme` and `expo.android.scheme` take precedence over the top-level field. If the field is omitted entirely, Expo Prebuild automatically uses `ios.bundleIdentifier` and `android.package` as fallback schemes.
What is the difference between a custom URL scheme and a Universal Link?
A custom URL scheme (e.g., `myapp://`) is a proprietary protocol only your app handles. If the app is not installed, the link fails silently with no fallback. A Universal Link (iOS) or App Link (Android) uses a standard `https://` URL — if the app is installed the OS opens it directly; if not, the user reaches your website. Universal Links are the correct choice for production marketing links, transactional emails, and password-reset flows where a graceful web fallback matters.
Do I need to rebuild my Expo app after changing the linking configuration?
Yes. The `scheme`, `associatedDomains`, and `intentFilters` fields are all read at native build time and compiled into the binary. Changes do not reflect through Expo Go or Metro hot-reload. After editing these fields run `npx expo prebuild –clean` then rebuild with `npx expo run:ios`, `npx expo run:android`, or `npx eas build`.
Can I test Universal Links without deploying to a real public server?
Yes, using Expo’s tunnel mode. Run `EXPO_TUNNEL_SUBDOMAIN=mysubdomain npx expo start –tunnel` to get a stable HTTPS Ngrok URL, host your AASA at that domain, and set `associatedDomains` to `applinks:mysubdomain.ngrok.io`. You still need a physical iOS device — simulators do not reliably verify AASA files. Also send the test link from iMessage or another app, not from Safari on the same domain, or the OS will not trigger Universal Links.
Why do my Android App Links show a disambiguation dialog instead of opening the app?
The most common cause is a missing `autoVerify: true` in your intent filter. Without it Android skips domain verification and shows the chooser. Also confirm that `assetlinks.json` is served over HTTPS with `Content-Type: application/json`, that the SHA-256 fingerprint matches the signing key of the installed APK, and that you have included the Google Play App Signing key fingerprint in addition to your upload key fingerprint if you use Play App Signing.
Get More from expo deep linking setup
Log the coasters, stadiums, and venues you’ve experienced, rate expo deep linking setup, and see what your friends thought. Get the ThrillZing app.