React Native deep linking that works in production needs three things. You need a verification file on your domain, native config on each platform, and JavaScript that handles both cold and warm starts. Get any one wrong and the link opens a browser instead of your app, and nothing tells you why.
Why links open the browser instead of the app
Custom URL schemes like myapp:// are easy to add, but any app can claim them. They also fail when the app is not installed. Universal Links on iOS and App Links on Android use normal https URLs that your domain proves it owns, so they fall back to your website cleanly.
Custom scheme vs verified https links
- Custom scheme (myapp://): no server work. Another app can claim the same scheme. Broken if the app is missing. Fine for OAuth redirects and internal testing.
- Universal Links and App Links (https://): need a hosted file and signing details. Only your app can claim them. They open your website if the app is missing. Use these for email, SMS and marketing links.
We build this for clients, so if you would rather ship links that are verified and tested before launch, ask us about yours.
Prerequisites
- A bare React Native app, or Expo with prebuild or EAS Build. Expo Go cannot test Universal Links.
- A domain you control, served over HTTPS with a valid certificate.
- Your Apple Team ID and bundle ID, for example ABCDE12345 and com.example.shop.
- Your Android package name and the SHA-256 fingerprints of every key that signs the app.
- React Navigation 6 or 7. It is optional, but the routing step assumes it.
Setting up React Native deep linking in five steps
1. Host the apple-app-site-association file
Serve this file at https://example.com/.well-known/apple-app-site-association with no file extension: {"applinks": {"details": [{"appIDs": ["ABCDE12345.com.example.shop"], "components": [{"/": "/orders/*"}]}]}}
The appIDs entry is your Team ID plus bundle ID. The components array limits which paths open the app, so /orders/42 opens it and /blog stays in the browser. Serve the file with Content-Type application/json, and return it directly with no redirect.
2. Add Associated Domains and the iOS handler
In Xcode, add the Associated Domains capability with the entry applinks:example.com. Add one entry per host, because www.example.com and example.com count as different domains. In Expo, set ios.associatedDomains in app.json instead.
In a Swift AppDelegate, forward the activity to React Native: func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) }
The exact AppDelegate shape has changed across recent React Native templates, including whether you need override. Check the React Native Linking docs for your version, which also show the Objective-C form. Apple's Associated Domains documentation covers the full file format.
3. Host assetlinks.json
Serve this at https://example.com/.well-known/assetlinks.json: [{"relation": ["delegate_permission/common.handle_all_urls"], "target": {"namespace": "android_app", "package_name": "com.example.shop", "sha256_cert_fingerprints": ["AB:CD:..."]}}]
The fingerprints array accepts several values. List your debug key, your upload key and the Play App Signing key so every build verifies.
4. Add the Android intent filter
Inside your main activity in AndroidManifest.xml: <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="example.com" android:pathPrefix="/orders" /> </intent-filter>
The autoVerify flag tells Android to fetch assetlinks.json at install time. In Expo, put the same data in android.intentFilters with autoVerify set to true. The Android App Links guide explains the verification rules.
5. Route URLs in JavaScript
With React Navigation, pass a linking config: const linking = { prefixes: ['https://example.com'], config: { screens: { Order: 'orders/:id' } } }; then render <NavigationContainer linking={linking}>.
React Navigation reads the launch URL and listens for new ones, so Order receives params.id as "42". Without a navigation library, you must handle both cases yourself, as shown below.
The two mistakes that break links in production
The release build's key is not in assetlinks.json
With Play App Signing, Google re-signs your app with a key you never hold. The SHA-256 from your local keystore is the upload key, not the key users install. Copy the app signing fingerprint from the App signing page in Play Console, or verification fails and Android 12+ opens every link in the browser.
The iOS version of this bug is a redirect. If example.com redirects to www.example.com, the AASA fetch fails for the apex domain.
Cold-start links get dropped
The url event only fires when the app is already running. A link that launches a closed app arrives through getInitialURL instead. If you handle links manually, cover both:
useEffect(() => { Linking.getInitialURL().then(url => { if (url) handleUrl(url); }); const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url)); return () => sub.remove(); }, []);
Also wait until navigation is ready before you navigate. Otherwise a cold-start link can run before your screens mount, and the user lands on the home screen.
How to test links on both platforms
iOS
- Simulator: run xcrun simctl openurl booted https://example.com/orders/42 with the app installed.
- Check what Apple cached: curl https://app-site-association.cdn-apple.com/a/v1/example.com. Devices read this CDN copy, which can lag behind your server, sometimes by hours.
- To skip the CDN while developing, use applinks:example.com?mode=developer and turn on Associated Domains Development in the device's Developer settings.
- On a device, tap the link in Notes or Messages. Typing it into the Safari address bar never opens the app, by design.
Android
- Check verification: adb shell pm get-app-links com.example.shop. Each domain should show verified.
- Force a re-check after fixing the file: adb shell pm verify-app-links --re-verify com.example.shop.
- Open a link directly: adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/orders/42" com.example.shop.
Test each link twice: once with the app killed and once with it in the background. That covers both code paths from the previous section.
FAQ
Why does my universal link open Safari instead of my app?
Usually the AASA file is redirected, served with the wrong content type, or not yet updated on Apple's CDN. It also happens when you type the URL into Safari, or tap a link on a page from the same domain. Check the CDN URL with curl first.
Do I still need a custom URL scheme?
Often, yes. OAuth providers and some SDKs redirect to a custom scheme such as myapp://callback. Keep the scheme for those flows and use https links for anything a user shares or receives.
What replaces Firebase Dynamic Links?
Google shut down Firebase Dynamic Links in August 2025. For most apps, plain Universal Links and App Links plus a web fallback page cover the same need. If you need deferred deep links that survive an install, look at a dedicated attribution service.



