A Complete Mobile Developer Guide to Schemes, Universal Links, App Links, and Navigation Deep linking looks trivial in a hello-world demo. You add a custom scheme, tap a link, the app pops open—magic!
Then your marketing team runs an email campaign, a user taps a link in Gmail on iOS, it opens Safari with a 404 error instead of your app, and you spend your Friday evening reading Apple’s apple-app-site-association docs trying to figure out why.
KEY TAKEAWAY: Deep linking is 80% OS/server configuration and 20% React Native code. Here is how deep linking actually works, why links break, and how to set them up cleanly using modern React Native patterns.
1. The 3 Types of Deep Links
Before opening Xcode or Android Studio, clarify what you are building:
Custom Schemes (myapp://path): Simple setup. Works great for inter-app handoffs (like OAuth callbacks or Stripe returns). Problem: If the app isn't installed, the link breaks. Worse, email clients like Gmail routinely strip custom schemes.
Universal Links (iOS) & App Links (Android): Standard https:// URLs that open your app directly if installed. If not installed, they fall back smoothly to your website.
Deferred Deep Links: Tapping a link sends the user to the App Store, and after they install it, your app still lands them on the specific screen they initially tapped. Operating systems don't handle this out-of-the-box; you need tools like Branch.io or custom fingerprinted landing pages.
2. Server-Side Setup: Proving Domain Ownership
To use https:// links, both iOS and Android require you to host a static JSON file on your web domain. This proves to the OS that your app actually owns yourdomain.com.
iOS: apple-app-site-association (AASA)
Host this file at https://yourdomain.com/.well-known/apple-app-site-association
CRITICAL NOTE: No json extension in the file name! Ensure it is served with Content-Type: application/json.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "A1B2C3D4E5.com.yourcompany.yourapp",
"paths": ["/products/*", "/profile/*"]
}
]
}
}
Note: Replace A1B2C3D4E5 with your Apple Team ID and com.yourcompany.yourapp with your Bundle Identifier.
Android: assetlinks.json
Host this file at https://yourdomain.com/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"14:6D:E9:7C:..."
]
}
}
]
To get your SHA256 fingerprint, run:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
3. Configuring Native Code
iOS Setup
- In Xcode, select your app target → Signing & Capabilities → + Capability → Associated Domains.
- Add your domain prefixed with applinks: (e.g., applinks:yourdomain.com).
- Open AppDelegate.mm (or AppDelegate.swift) and make sure React Native's
RCTLinkingManagerreceives incoming URLs:
// AppDelegate.swift
import React
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
return RCTLinkingManager.application(app, open: url, options: options)
}
// Required for Universal Links
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}
Android Setup
Inside android/app/src/main/AndroidManifest.xml, update your MainActivity with an intent filter. Notice android:autoVerify="true"—this tells Android to fetch your assetlinks.json upon app installation.
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exportExported="true">
<!-- Custom Scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
<!-- App Links -->
<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="yourdomain.com" />
</intent-filter>
</activity>
4. Handling Links in React Native
If you are using React Navigation, deep link mapping is practically built-in. Define a linking configuration and pass it to your NavigationContainer.
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
const linking = {
prefixes: ['myapp://', 'https://yourdomain.com'],
config: {
screens: {
Home: 'home',
ProductDetails: {
path: 'products/:id',
parse: {
id: (id: string) => `prod_${id}`,
},
},
NotFound: '*',
},
},
};
export default function App() {
return (
<NavigationContainer linking={linking} fallback={<LoadingSpinner />}>
{/* Your Stack / Navigator */}
</NavigationContainer>
);
}
Manual Handling (Bare React Native Linking API)
If you aren't using React Navigation or need to parse custom logic before opening a screen:
import { useEffect } from 'react';
import { Linking } from 'react-native';
export function useDeepLinking() {
useEffect(() => {
// 1. App was closed and opened via deep link
Linking.getInitialURL().then((url) => {
if (url) handleUrl(url);
});
// 2. App was running in background/foreground
const subscription = Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
return () => subscription.remove();
}, []);
const handleUrl = (url: string) => {
console.log('Incoming URL:', url);
// Parse query params or path logic here
};
}
5. Testing & Debugging
TESTING TIP: Don't test by typing URLs directly into Safari/Chrome address bars! Browsers assume you are trying to visit a web page and won't trigger the OS handoff.
Instead paste a link on notes & press it. OR
iOS Simulator
Run this from your terminal while the simulator is running:
xcrun simctl openurl booted "myapp://products/42"
# Or test universal links:
xcrun simctl openurl booted https://yourdomain.com/products/42
Android Emulator / Real Device
adb shell am start -W -a android.intent.action.VIEW \
-d "https://yourdomain.com/products/42" com.yourcompany.yourapp
Troubleshooting Checklist
- App opens Safari instead of opening the app: Check if your apple-app-site-association file is returning a 200 OK with application/json headers. Apple caches this file heavily on installation; reinstall the app after making changes.
- Android App Links failing: Run adb shell pm get-app-links com.yourcompany.yourapp. If status says legacy or failed, your assetlinks.json SHA256 fingerprint doesn't match your app's signing key.
- Link wrapped by Email Tracking Services: SendGrid/Mailchimp rewrite URLs for click tracking. This breaks Universal Links because the domain changes to the tracker's domain. Enable SSL click tracking on a dedicated subdomain or configure domain-proxying in SendGrid.
Conclusion: Get It Right Before Launches
Deep linking isn't about writing complex React Native logic—it’s about getting the OS handshakes and domain verification right.
If you take away three rules from this guide:Always use Universal/App Links for public-facing URLs and keep custom schemes strictly for internal or OAuth callbacks.
Double-check your server headers—Apple and Android won't verify your domain if your JSON files return the wrong Content-Type or redirect.Test early via CLI, not browser URL bars, and build a staging test link setup into your CI pipeline.
Top comments (0)