Mobile App Deep Linking
Implement deep linking that navigates users to specific content within your mobile app. Covers URI schemes, Universal Links, App Links, deferred deep links, attribution, and the patterns that create seamless navigation between web and native app.
Deep linking takes users directly to specific content inside your app instead of the home screen. A link like myapp://orders/123 or https://myapp.com/orders/123 opens the order detail screen, not the login page. Done well, deep links create seamless transitions between web, email, push notifications, and the app itself.
Deep Link Types
URI Scheme (Custom):
myapp://orders/123
- Works only if app is installed
- Cannot verify ownership
- Falls back to nothing (error in browser)
- ⚠ Deprecated for most use cases
Universal Links (iOS):
https://myapp.com/orders/123
- Opens app if installed, website if not
- Verified ownership (apple-app-site-association)
- Seamless web-to-app transition
App Links (Android):
https://myapp.com/orders/123
- Opens app if installed, website if not
- Verified ownership (assetlinks.json)
- Same URL works on both platforms
Deferred Deep Links:
User clicks link → App not installed → App Store → Install →
App opens to SPECIFIC content from original link
- Requires deep link service (Branch, Firebase Dynamic Links)
iOS Universal Links
// apple-app-site-association (AASA)
// Hosted at: https://myapp.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.company.myapp",
"paths": [
"/orders/*",
"/products/*",
"/profile/*",
"NOT /api/*",
"NOT /static/*"
]
}
]
}
}
// Handle Universal Links in iOS
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
handleDeepLink(url)
}
func handleDeepLink(_ url: URL) {
let pathComponents = url.pathComponents
switch pathComponents.first {
case "orders":
if let orderId = pathComponents.last {
navigator.navigate(to: .orderDetail(id: orderId))
}
case "products":
if let productId = pathComponents.last {
navigator.navigate(to: .productDetail(id: productId))
}
default:
navigator.navigate(to: .home)
}
}
}
Android App Links
// assetlinks.json
// Hosted at: https://myapp.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.company.myapp",
"sha256_cert_fingerprints": [
"AB:CD:EF:12:34:56:..."
]
}
}]
// Handle App Links in Android
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.data?.let { uri ->
handleDeepLink(uri)
}
}
private fun handleDeepLink(uri: Uri) {
when {
uri.path?.startsWith("/orders/") == true -> {
val orderId = uri.lastPathSegment
navigator.navigateTo(OrderDetailScreen(orderId))
}
uri.path?.startsWith("/products/") == true -> {
val productId = uri.lastPathSegment
navigator.navigateTo(ProductDetailScreen(productId))
}
else -> navigator.navigateTo(HomeScreen)
}
}
}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Only URI schemes | No fallback if app not installed | Universal Links / App Links |
| No deferred deep links | Post-install loses original intent | Branch, Firebase Dynamic Links |
| Auth wall on deep link | User logs in, loses context | Queue deep link, resume after auth |
| No deep link validation | Broken links in production | Automated deep link testing |
| Different URLs per platform | Fragmented link management | Single URL, platform-handled routing |
Deep linking is the connective tissue between every channel that reaches your users — web, email, push, ads, social, and QR codes. Every entry point should land the user exactly where they intended to go.