ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Deep Linking: Connecting the Web to Your Mobile App

Implement deep linking that seamlessly routes users from web links, emails, and notifications into specific screens in your mobile app. Covers Universal Links, App Links, deferred deep linking, link routing architectures, and the testing strategies that prevent deep links from silently breaking.

A deep link is a URL that opens a specific screen in your mobile app instead of a generic web page. When a user taps a link in an email, a notification, or a social media post, deep linking takes them directly to the relevant content — not to the app’s home screen where they have to navigate manually.

Deep linking converts external touchpoints (marketing emails, push notifications, social shares) into in-app engagement. Without it, every external link sends users to the web or to a generic app launch.


myapp://orders/456
myapp://products/boots-vintage?color=black

Pros: Simple to implement. Cons: Only works if the app is installed. Uninstalled users see an error.

Standard HTTPS URLs that open the app when installed and fall back to the web when not:

https://example.com/orders/456

On iOS, if the app is installed, this opens the app directly. If not, it opens Safari showing the web version. Same behavior on Android with App Links.

Route users to the correct screen even if the app is not yet installed:

1. User taps https://example.com/orders/456
2. App is not installed → redirect to App Store
3. User installs the app
4. App opens → navigates to orders/456 (the original destination)

This requires a deep linking service that stores the intent across the install boundary.


Configuration

  1. Apple App Site Association (AASA) file on your web server:
// https://example.com/.well-known/apple-app-site-association
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": [
          "/orders/*",
          "/products/*",
          "/profile/*"
        ]
      }
    ]
  }
}
  1. Entitlement in the app:
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:example.com</string>
</array>
  1. Handle the link in AppDelegate:
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }
    
    return DeepLinkRouter.handle(url)
}

Configuration

  1. Digital Asset Links file on your web server:
// https://example.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": ["AB:CD:EF:..."]
  }
}]
  1. Intent filter in AndroidManifest.xml:
<activity android:name=".MainActivity">
    <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>
</activity>
  1. Handle in Activity:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    intent?.data?.let { uri ->
        DeepLinkRouter.handle(uri)
    }
}

Centralize deep link handling in a router:

class DeepLinkRouter {
    
    static func handle(_ url: URL) -> Bool {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            return false
        }
        
        let pathComponents = components.path.split(separator: "/").map(String.init)
        
        switch pathComponents.first {
        case "orders":
            if let orderId = pathComponents.dropFirst().first {
                navigateTo(.orderDetail(id: orderId))
                return true
            }
            navigateTo(.orderList)
            return true
            
        case "products":
            if let productSlug = pathComponents.dropFirst().first {
                let color = components.queryItems?.first(where: { $0.name == "color" })?.value
                navigateTo(.productDetail(slug: productSlug, color: color))
                return true
            }
            navigateTo(.productCatalog)
            return true
            
        case "profile":
            navigateTo(.profile)
            return true
            
        default:
            return false
        }
    }
}

Handling Authentication

Some deep links require authentication:

case "orders":
    if authManager.isLoggedIn {
        navigateTo(.orderDetail(id: orderId))
    } else {
        // Save destination, show login, then redirect
        pendingDeepLink = .orderDetail(id: orderId)
        navigateTo(.login)
    }

After successful login, check for a pending deep link and navigate.


Deferred Deep Linking

For users who do not have the app installed:

Tap link → Web landing page

"Open in App" banner (Smart App Banner on iOS)

Install from App Store / Play Store

First launch → navigate to original deep link destination

Implementation Options

  • Branch.io: Full-featured deep linking platform with attribution
  • Firebase Dynamic Links (deprecated, migrating to short links)
  • Custom solution: Store link data in clipboard or server-side session

Custom Deferred Deep Linking

// Web: Store deep link intent before redirect to app store
const deepLinkData = {
    destination: '/orders/456',
    timestamp: Date.now(),
    campaign: 'spring_sale'
};

// Store in server session tied to device fingerprint
fetch('/api/deferred-deeplink', {
    method: 'POST',
    body: JSON.stringify(deepLinkData)
});

// Redirect to app store
window.location = 'https://apps.apple.com/app/myapp/id123456';
// App: Check for deferred deep link on first launch
func handleFirstLaunch() {
    api.checkDeferredDeepLink(deviceId: getDeviceId()) { result in
        if let destination = result?.destination {
            DeepLinkRouter.navigate(to: destination)
        }
    }
}

Manual Testing

# iOS Simulator
xcrun simctl openurl booted "https://example.com/orders/456"

# Android Emulator
adb shell am start -W -a android.intent.action.VIEW \
  -d "https://example.com/orders/456" com.example.myapp

Automated Testing

// XCTest: Verify deep link routing
func testOrderDeepLink() {
    let url = URL(string: "https://example.com/orders/456")!
    let handled = DeepLinkRouter.handle(url)
    
    XCTAssertTrue(handled)
    XCTAssertEqual(navigationController.topViewController, OrderDetailViewController.self)
    XCTAssertEqual(currentOrderId, "456")
}

Validation Checklist

  • AASA/assetlinks.json is accessible via HTTPS (not redirect)
  • Content-Type is application/json (not text/html)
  • App ID and package name match exactly
  • All deep link paths are covered in routing
  • Authentication redirect preserves the original destination
  • Fallback to web works when app is not installed

Anti-Patterns

Anti-PatternConsequenceFix
URI schemes onlyBroken for uninstalled usersUniversal Links + App Links
No fallback web pageDead link for non-app usersEvery deep link has a web equivalent
Hardcoded routesBrittle, hard to updateCentralized router with pattern matching
No auth handlingLogin wall drops the deep linkQueue deep link, redirect after auth
No automated testingSilent breakage on refactorCI tests for all deep link patterns

Deep linking is the bridge between your marketing channels and your app experience. Every email, every notification, every social share is an opportunity to land users exactly where they need to be. When it works, it feels like magic. When it breaks, it feels like a dead end.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →