Universal Links vs App Links
Universal Links (iOS) and App Links (Android) are the modern, platform-native approaches to deep linking. Unlike custom URL schemes, they use standard HTTPS URLs that work as both web links and app deep links, providing a seamless experience across all scenarios.
Quick Comparison
| Feature | Universal Links (iOS) | App Links (Android) |
|---|---|---|
| Platform | iOS 9+ | Android 6.0+ (API 23+) |
| URL Format | https://yourdomain.com/path | https://yourdomain.com/path |
| Verification File | apple-app-site-association | assetlinks.json |
| File Location | /.well-known/ | /.well-known/ |
| Fallback | Opens Safari if app not installed | Opens browser if app not installed |
| Requires Domain | ✅ Yes | ✅ Yes |
| HTTPS Required | ✅ Yes | ✅ Yes |
| User Prompt | No (seamless) | No (if verified) |
Key Similarity: Both use standard HTTPS URLs that work everywhere
Key Difference: Implementation details and verification processes differ
Why Universal Links / App Links?
The Problem with Custom URL Schemes
Old approach (custom schemes):
myapp://products/123
Problems:
- ❌ Only works if app installed (broken link otherwise)
- ❌ Scheme conflicts (multiple apps can claim same scheme)
- ❌ Browser security warnings
- ❌ Can't use as regular web links
- ❌ Poor sharing experience
The Modern Approach (HTTPS Links)
Universal/App Links:
https://go.yourapp.com/products/123
Benefits:
- ✅ Works as regular web link (shareable everywhere)
- ✅ If app installed → Opens app
- ✅ If app NOT installed → Opens website
- ✅ No broken links ever
- ✅ No scheme conflicts (you own the domain)
- ✅ Better user experience
- ✅ SEO-friendly (search engines index them)
Universal Links (iOS)
What Are Universal Links?
Universal Links are Apple's implementation of HTTPS-based deep linking, introduced in iOS 9 (2015). They allow you to associate your iOS app with your website domain.
How Universal Links Work
1. User clicks: https://go.yourapp.com/products/123
2. iOS checks:
- Is an app associated with "go.yourapp.com"?
- Does that app have Universal Links enabled?
3a. If YES:
→ App opens directly
→ App receives URL: /products/123
→ App routes user to product page
3b. If NO (app not installed):
→ Safari opens
→ Loads: https://go.yourapp.com/products/123
→ Website displays product page
Result: Seamless experience whether app is installed or not
Universal Links Requirements
1. Owned Domain
You must own a domain (e.g., go.yourapp.com or yourapp.com)
Options:
- Use main domain:
yourapp.com - Use subdomain:
go.yourapp.com,link.yourapp.com,app.yourapp.com
Recommendation: Use subdomain dedicated to links (easier to manage)
2. HTTPS Hosting
Your domain must be accessible via HTTPS.
Requirements:
- ✅ Valid SSL certificate
- ✅ HTTPS enabled
- ✅ Publicly accessible (not localhost)
3. Apple App Site Association (AASA) File
A JSON file that tells iOS which app is associated with your domain.
File name: apple-app-site-association (no .json extension)
Location: https://yourdomain.com/.well-known/apple-app-site-association
Example:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM1234.com.yourcompany.yourapp",
"paths": [
"/products/*",
"/offers/*",
"NOT /admin/*"
]
}
]
}
}
Components:
appID: Team ID + Bundle ID (format:TEAMID.BUNDLEID)paths: Which URL paths should open the app"/products/*"→ Matches all product pages"NOT /admin/*"→ Excludes admin pages (stay in browser)
4. Associated Domains in Xcode
Enable Associated Domains capability and add your domain.
In Xcode:
- Select your project → Target → Signing & Capabilities
- Click "+ Capability"
- Add "Associated Domains"
- Add domain with prefix:
applinks:go.yourapp.com
Example:
applinks:go.yourapp.com
applinks:yourapp.com
No HTTPS prefix needed (iOS adds it automatically)
5. Handle Universal Links in Code
Implement AppDelegate method to receive URLs.
Swift (UIKit):
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
// Handle the URL
// url.path will be something like "/products/123"
handleUniversalLink(url: url)
return true
}
func handleUniversalLink(url: URL) {
let path = url.path
if path.hasPrefix("/products/") {
let productID = path.replacingOccurrences(of: "/products/", with: "")
// Navigate to product screen
navigateToProduct(id: productID)
} else if path.hasPrefix("/offers/") {
// Navigate to offers
navigateToOffers()
}
}
SwiftUI:
@main
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleUniversalLink(url: url)
}
}
}
}
Universal Links Verification Process
When does iOS download the AASA file?
- When app is first installed
- When app is updated
- Periodically (iOS manages this)
Important:
- AASA file is cached by iOS (changes can take hours/days to propagate)
- For testing, uninstall and reinstall app to force re-download
Universal Links Troubleshooting
Problem: Universal Links Not Working
Checklist:
-
Is AASA file accessible?
curl https://go.yourapp.com/.well-known/apple-app-site-association- Should return JSON (200 OK)
- No redirects (301/302 not allowed)
- HTTPS required
- Correct Content-Type header (application/json or no Content-Type)
-
Is Team ID correct?
- Find in Apple Developer account
- Format: 10 characters (e.g.,
AB12CD34EF) - Common mistake: Using wrong Team ID
-
Is Bundle ID correct?
- Must match exactly (case-sensitive)
- Format:
com.yourcompany.yourapp
-
Is Associated Domains configured in Xcode?
- Check Signing & Capabilities
- Should see:
applinks:go.yourapp.com
-
Is app code implemented?
- Check AppDelegate method exists
- Add logging to verify it's being called
-
Is iOS caching old AASA?
- Uninstall app completely
- Reinstall from Xcode
- Test again
-
Test with Apple's validator
- Visit: https://search.validator.apple.com/
- Enter your URL
- Check for errors
Problem: Links Open in Safari Instead of App
Reasons:
-
User manually disabled Universal Links
- When user long-presses a Universal Link, they can choose "Open in Safari"
- iOS remembers this preference
- Fix: Uninstall and reinstall app
-
Link opened from same domain
- If user is already on
go.yourapp.comwebsite - And clicks a link to
go.yourapp.com/products/123 - iOS keeps them in Safari (user intent to stay on web)
- Fix: Use a different domain for website vs links
- If user is already on
-
AASA file has errors
- Check with Apple's validator
- Ensure JSON is valid
App Links (Android)
What Are App Links?
App Links are Android's implementation of HTTPS-based deep linking, introduced in Android 6.0 (2015). They work similarly to Universal Links but with Android-specific setup.
How App Links Work
1. User clicks: https://go.yourapp.com/products/123
2. Android checks:
- Is an app associated with "go.yourapp.com"?
- Is the app verified for this domain?
3a. If YES (verified):
→ App opens directly
→ App receives URL: https://go.yourapp.com/products/123
→ App routes user to product page
3b. If NO (app not installed or not verified):
→ Browser opens
→ Loads: https://go.yourapp.com/products/123
→ Website displays product page
App Links Requirements
1. Owned Domain
Same as iOS - you must own the domain.
2. HTTPS Hosting
Same as iOS - HTTPS required with valid SSL certificate.
3. Digital Asset Links File
A JSON file that verifies your app is authorized to open links for this domain.
File name: assetlinks.json
Location: https://yourdomain.com/.well-known/assetlinks.json
Example:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
]
}
}
]
Components:
relation: Always["delegate_permission/common.handle_all_urls"]package_name: Android package name (e.g.,com.yourcompany.yourapp)sha256_cert_fingerprints: SHA-256 fingerprint of your app signing key
4. Get SHA-256 Fingerprint
For debug builds:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
For release builds:
keytool -list -v -keystore /path/to/your-release-key.keystore -alias your-key-alias
Output will include:
SHA256: FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C
Copy this into assetlinks.json.
Important: Debug and release builds have different fingerprints!
5. Configure AndroidManifest.xml
Add intent filters for your domain URLs.
AndroidManifest.xml:
<activity android:name=".MainActivity">
<!-- Other intent filters... -->
<!-- App Links Intent Filter -->
<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="go.yourapp.com" />
</intent-filter>
</activity>
Key attribute: android:autoVerify="true"
- Tells Android to verify domain association
- Without this, user gets prompted to choose browser vs app
Multiple domains:
<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="go.yourapp.com" />
<data android:scheme="https" android:host="yourapp.com" />
</intent-filter>
6. Handle App Links in Code
Kotlin (MainActivity):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Handle intent
handleIntent(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { handleIntent(it) }
}
private fun handleIntent(intent: Intent) {
val action = intent.action
val data = intent.data
if (Intent.ACTION_VIEW == action && data != null) {
// data.path will be something like "/products/123"
handleDeepLink(data)
}
}
private fun handleDeepLink(uri: Uri) {
val path = uri.path
when {
path?.startsWith("/products/") == true -> {
val productID = path.replace("/products/", "")
// Navigate to product screen
navigateToProduct(productID)
}
path?.startsWith("/offers/") == true -> {
// Navigate to offers
navigateToOffers()
}
}
}
React Native:
import { Linking } from 'react-native';
useEffect(() => {
// Handle initial URL (app was closed)
Linking.getInitialURL().then(url => {
if (url) {
handleDeepLink(url);
}
});
// Handle URL when app is running
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
const handleDeepLink = (url: string) => {
const path = url.replace('https://go.yourapp.com', '');
if (path.startsWith('/products/')) {
const productID = path.replace('/products/', '');
navigation.navigate('Product', { id: productID });
}
};
App Links Verification Process
When does Android verify?
- When app is installed
- User can manually verify in Settings
Check verification status:
adb shell pm get-app-links com.yourcompany.yourapp
Output:
com.yourcompany.yourapp:
ID: ...
Signatures: ...
Domain verification state:
go.yourapp.com: verified ← This is what you want
If shows "none" or "legacy":
- assetlinks.json not accessible
- SHA-256 fingerprint mismatch
- android:autoVerify not set to true
App Links Troubleshooting
Problem: App Links Not Working
Checklist:
-
Is assetlinks.json accessible?
curl https://go.yourapp.com/.well-known/assetlinks.json- Should return JSON (200 OK)
- HTTPS required
- No redirects
-
Is package name correct?
- Check in AndroidManifest.xml
- Must match exactly
-
Is SHA-256 fingerprint correct?
- Run keytool command
- Copy fingerprint to assetlinks.json
- Common mistake: Using debug fingerprint for release build (or vice versa)
-
Is android:autoVerify="true" set?
- Check AndroidManifest.xml intent filter
-
Force verification:
adb shell pm verify-app-links --re-verify com.yourcompany.yourapp -
Check verification status:
adb shell pm get-app-links com.yourcompany.yourapp -
Test with adb:
adb shell am start -a android.intent.action.VIEW -d "https://go.yourapp.com/products/123"- Should open your app (not browser)
Problem: Disambiguation Dialog Appears
When this happens:
User clicks link → Android shows:
"Open with:"
[ Chrome ]
[ Your App ]
Causes:
android:autoVerify="true"not set- Domain not verified (assetlinks.json issue)
- User manually cleared defaults
Fix: Ensure verification completes successfully
Universal Links vs App Links: Key Differences
1. Verification File Format
Universal Links (AASA):
{
"applinks": {
"apps": [],
"details": [{
"appID": "TEAMID.com.yourcompany.yourapp",
"paths": ["/products/*"]
}]
}
}
App Links (assetlinks):
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": ["FA:C6:17:..."]
}
}]
2. Path Filtering
Universal Links: Can specify which paths open app
"paths": [
"/products/*", // Opens app
"/offers/*", // Opens app
"NOT /admin/*" // Stays in browser
]
App Links: All paths under domain open app (configured in AndroidManifest)
<data android:scheme="https" android:host="go.yourapp.com" />
<!-- All paths under go.yourapp.com/* will try to open app -->
3. Caching
Universal Links:
- iOS caches AASA file
- Changes can take hours/days
- Must uninstall/reinstall to force refresh
App Links:
- Android verifies on install
- Can manually re-verify
- Faster to test changes
4. Debugging
Universal Links:
- Use Apple's validator: https://search.validator.apple.com/
- Check Console app for logs
- More opaque (harder to debug)
App Links:
- Use adb commands to check status
- Force re-verification
- Easier to debug
Hosting Both Files
You need BOTH files for cross-platform apps:
https://go.yourapp.com/
.well-known/
apple-app-site-association (iOS)
assetlinks.json (Android)
Static file hosting (recommended):
- AWS S3 + CloudFront
- Netlify
- Vercel
- GitHub Pages
Web server configuration:
- Ensure HTTPS enabled
- No redirects for .well-known paths
- Correct MIME types (application/json or no Content-Type)
How LinkForty Simplifies This
The Manual Approach is Complex
Without a platform:
- Set up domain and HTTPS hosting
- Generate AASA file (get Team ID, Bundle ID, paths)
- Generate assetlinks.json (get package name, SHA-256 fingerprint)
- Host both files at
/.well-known/ - Configure Xcode (Associated Domains)
- Configure AndroidManifest.xml (intent filters)
- Implement handling code in both iOS and Android
- Test thoroughly on both platforms
- Debug issues (often opaque error messages)
Time: 1-2 weeks for first implementation
LinkForty Auto-Generates Verification Files
With LinkForty:
-
Configure once in dashboard:
- iOS Team ID:
TEAM1234 - iOS Bundle ID:
com.yourapp - Android Package Name:
com.yourapp - Android SHA-256 Fingerprint:
FA:C6:17:... - Domain:
go.yourapp.com
- iOS Team ID:
-
LinkForty automatically generates:
/.well-known/apple-app-site-association/.well-known/assetlinks.json
-
Point your domain to LinkForty:
- CNAME:
go.yourapp.com→your-org.linkforty.app - Or use LinkForty's hosting
- CNAME:
-
Implement SDK (handles all platform-specific code):
import LinkForty from '@linkforty/react-native-sdk';
await LinkForty.init({
baseUrl: 'https://api.linkforty.com',
apiKey: 'your-api-key'
});
// LinkForty SDK handles Universal Links + App Links automatically
Time: 15 minutes setup + testing
Best Practices
1. Use Dedicated Subdomain
Don't: Use your main website domain
https://yourapp.com/products/123
Problem: If user is browsing yourapp.com website, links won't open app (iOS keeps them in browser)
Do: Use dedicated subdomain
https://go.yourapp.com/products/123
https://link.yourapp.com/products/123
https://app.yourapp.com/products/123
Benefit: Clear separation between website and app links
2. Keep Verification Files Accessible
Don't:
- Add authentication to
/.well-known/paths - Use redirects (301/302)
- Change files frequently
Do:
- Allow public access (no auth)
- Direct access (no redirects)
- Keep files stable (changes take time to propagate)
3. Test on Real Devices
Don't: Only test on simulator/emulator
- Simulators don't fully emulate Universal Links behavior
- Emulators may have different network setup
Do: Test on real devices
- iPhone (iOS)
- Android phone (Android)
- Test both Wi-Fi and cellular
- Test with app installed and uninstalled
4. Implement Proper Fallbacks
Always handle:
- App not installed → Open website
- URL path not recognized → Show home screen or error
- Network failure → Cache last known state
5. Monitor Verification Status
iOS: Check with Apple's validator regularly Android: Check verification status with adb
Summary
Universal Links (iOS) and App Links (Android) are the modern standard for mobile deep linking. They use HTTPS URLs that work everywhere, providing seamless experiences whether the app is installed or not.
Key Takeaways:
- Both platforms use HTTPS URLs (not custom schemes)
- Require domain ownership and HTTPS hosting
- Need verification files hosted at
/.well-known/ - Provide graceful fallbacks to web
- LinkForty auto-generates verification files (saves 1-2 weeks of setup)
Ready to implement?
LinkForty handles all the complexity:
- ✅ Auto-generates AASA + assetlinks.json files
- ✅ Cross-platform SDK (handles both iOS and Android)
- ✅ Testing tools and validation
- ✅ 15-minute setup vs 1-2 weeks manual
View Implementation Tutorial →
Next Steps
Learn:
- What is Deep Linking? - Deep linking fundamentals
- Deferred Deep Linking - Post-install attribution
Implement:
- Implement Universal Links (iOS) - Step-by-step iOS guide
- Implement App Links (Android) - Step-by-step Android guide
- SDK Integration Guide - LinkForty SDK setup
Questions?