Universal Links are Apple's mechanism for opening your app when a user taps an HTTPS link to your domain. Unlike custom URL schemes, Universal Links are secure (only you can claim your domain), reliable (they fall back to the web if the app isn't installed), and don't show a disambiguation dialog.
This guide focuses on the iOS-specific setup for Universal Links in React Native. For the full cross-platform setup (including Android App Links), see React Native Deep Linking: Complete Setup Tutorial. For Universal Links fundamentals, see Universal Links: Everything You Need to Know.
How Universal Links Work
The flow when a user taps a Universal Link:
- User taps an HTTPS link (in Safari, Mail, Messages, or another app)
- iOS checks its local cache to see if any installed app has claimed this domain
- If the app is installed and claims the domain, iOS opens the app and passes the URL
- If the app is not installed, iOS opens the URL in Safari (normal web behavior)
The "claim" happens through two pieces:
- Your app declares which domains it handles (Associated Domains entitlement)
- Your domain declares which apps can handle it (AASA file)
Step 1: Apple App Site Association (AASA) File
Your web server (or deep linking platform) must serve a JSON file at:
https://go.yourapp.com/.well-known/apple-app-site-association
The file content:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["TEAMID.com.yourapp.bundleid"],
"components": [
{ "/": "/product/*" },
{ "/": "/invite/*" },
{ "/": "/promo/*" }
]
}
]
}
}
If using Tolinku, the AASA file is served automatically when you configure your Appspace with your Team ID and Bundle ID.
AASA Requirements
- Must be served over HTTPS (no HTTP, no redirects)
- Must have
Content-Type: application/json - Must be accessible without authentication
- Must not exceed 128 KB
- Must not be behind a redirect (Apple's crawler won't follow redirects)
Verify the AASA
# Check accessibility
curl -I https://go.yourapp.com/.well-known/apple-app-site-association
# Check content
curl https://go.yourapp.com/.well-known/apple-app-site-association | python3 -m json.tool
Apple's CDN Cache
Apple caches AASA files through their CDN. When you first deploy or update your AASA, it can take up to 24-48 hours for Apple to re-fetch it. Check what Apple currently has cached:
curl "https://app-site-association.cdn-apple.com/a/v1/go.yourapp.com"
To request a re-crawl, use Apple's Search Validation tool and enter your domain.
Step 2: Associated Domains Entitlement
In Xcode
- Open
ios/YourApp.xcworkspacein Xcode - Select your app target
- Go to "Signing & Capabilities"
- Click "+ Capability"
- Add "Associated Domains"
- Add:
applinks:go.yourapp.com
This generates (or updates) the YourApp.entitlements file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:go.yourapp.com</string>
</array>
</dict>
</plist>
Multiple Domains
If your links come from multiple domains:
<array>
<string>applinks:go.yourapp.com</string>
<string>applinks:links.yourapp.com</string>
</array>
Developer Mode (iOS 14+)
For development and testing, you can use the ?mode=developer suffix:
<string>applinks:go.yourapp.com?mode=developer</string>
This tells iOS to fetch the AASA file directly from your server instead of Apple's CDN, which is useful during development when you're frequently updating the file.
Step 3: AppDelegate Configuration
React Native's RCTLinkingManager bridges the native Universal Link callback to JavaScript.
For Objective-C AppDelegate (AppDelegate.mm)
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
// Handle Universal Links
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
// Handle custom URL schemes (optional, but recommended as fallback)
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
@end
For Swift AppDelegate (AppDelegate.swift)
import React
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return RCTLinkingManager.application(app, open: url, options: options)
}
}
SceneDelegate (iOS 13+)
If your app uses UISceneDelegate (the default for newer React Native projects):
// SceneDelegate.m
#import <React/RCTLinkingManager.h>
- (void)scene:(UIScene *)scene
continueUserActivity:(NSUserActivity *)userActivity {
[RCTLinkingManager application:[UIApplication sharedApplication]
continueUserActivity:userActivity
restorationHandler:^(NSArray<id<UIUserActivityRestoring>> * _Nullable restorableObjects) {
}];
}
- (void)scene:(UIScene *)scene
openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
for (UIOpenURLContext *context in URLContexts) {
[RCTLinkingManager application:[UIApplication sharedApplication]
openURL:context.URL
options:@{}];
}
}
Step 4: JavaScript Link Handling
Receiving Universal Links
import { Linking } from 'react-native';
// Cold start: app was launched by the link
useEffect(() => {
Linking.getInitialURL().then((url) => {
if (url) {
handleUniversalLink(url);
}
});
}, []);
// Warm start: app was already running
useEffect(() => {
const subscription = Linking.addEventListener('url', ({ url }) => {
handleUniversalLink(url);
});
return () => subscription.remove();
}, []);
Parsing Universal Link URLs
Universal Links arrive as full HTTPS URLs:
function handleUniversalLink(url) {
const parsed = new URL(url);
// Verify it's your domain
if (parsed.hostname !== 'go.yourapp.com') {
return;
}
const path = parsed.pathname;
const params = Object.fromEntries(parsed.searchParams);
// Route to the correct screen
if (path.startsWith('/product/')) {
const id = path.split('/')[2];
navigation.navigate('Product', { id });
} else if (path.startsWith('/invite/')) {
const code = path.split('/')[2];
navigation.navigate('Invite', { code });
} else {
// Unknown path: navigate to home or show not-found
navigation.navigate('Home');
}
}
Troubleshooting
Universal Links Don't Open the App
Check the AASA file:
- Is it accessible at
https://yourdomain/.well-known/apple-app-site-association? - Does it return
Content-Type: application/json? - Does the
appIDmatch your Team ID + Bundle ID exactly? - Is the path pattern in
components(orpathsfor the older format) correct?
Check the entitlement:
- Does the Associated Domains entry exactly match your domain? (no
https://, no trailing slash) - Is the entitlement present in the correct build target (not just the test target)?
Check the device:
- Universal Links don't work in the iOS Simulator for all scenarios. Test on a physical device.
- Delete and reinstall the app. iOS fetches the AASA on install and caches it.
- Check if another app on the device is claiming the same domain.
Universal Links Open Safari Instead
This happens when:
- The user typed the URL into Safari's address bar (Universal Links don't trigger from the address bar, by design)
- The user previously tapped the breadcrumb in Safari to "Open in Safari" for your domain, which disables Universal Links for that domain until they long-press a link and choose "Open in [App]"
- The AASA is cached with an old or invalid configuration
Universal Links Work in Some Apps but Not Others
Different apps handle Universal Links differently:
- Messages, Notes, Mail: Fully support Universal Links (tapping a link opens the app)
- Safari: Only works when navigating from another page, not from the address bar
- Some third-party apps: May use
SFSafariViewControllerorWKWebView, which have different Universal Link behavior
Cold Start Links Not Received
If links work when the app is backgrounded but not when launching the app fresh:
- Verify that
continueUserActivity(or the SceneDelegate equivalent) is called beforeRCTBridgefinishes initialization - Verify
Linking.getInitialURL()is called early in your app's JavaScript entry point - Check that the method is implemented in the correct delegate (AppDelegate vs SceneDelegate)
Testing Checklist
- AASA file accessible at
/.well-known/apple-app-site-association - AASA returns
Content-Type: application/json -
appIDsmatches Team ID + Bundle ID - Associated Domains entitlement includes
applinks:yourdomain -
continueUserActivityimplemented in AppDelegate/SceneDelegate - Cold start links received via
Linking.getInitialURL() - Warm start links received via
Linking.addEventListener - Tested from Notes, Messages, and Mail on physical device
- App deleted and reinstalled after AASA changes
For Android App Links (the Android equivalent), see the Android section in React Native Deep Linking. For the Tolinku SDK, see the React Native SDK docs. For deep linking features, see Tolinku deep linking.
Get deep linking tips in your inbox
One email per week. No spam.