guides

Universal Link vs deep link vs App Link: the three-way distinction

the linkboo team·6 min read·updated Mon Jun 01 2026 17:00:00 GMT-0700 (Pacific Daylight Time)
On this page

scope

Three terms — Universal Link, App Link, deep link — used interchangeably in product specs, marketing copy, and developer docs. They are not interchangeable. This guide draws the three-way distinction, gives the iOS and Android equivalents, and shows when each fails.

Pair page: /guides/universal-links-vs-deep-links covers the two-way distinction and the underlying mechanics in more depth. Cluster C hub: /guides/firebase-dynamic-links-replacement.

one-paragraph summary

A deep link is a URL — any URL — that opens a specific in-app screen. Originally meant a custom URL scheme like yourapp://product/123. A Universal Link is Apple's HTTPS-verified version of that, iOS-only, requiring an Apple App Site Association file on your domain. An App Link is Google's HTTPS-verified equivalent, Android-only, requiring an assetlinks.json file. "Deep link" is the umbrella term; "Universal Link" and "App Link" are the platform-specific verified mechanisms.

the table

Term Owner Platforms URL form Verified Required file Falls back to web
Deep link (custom scheme) Generic iOS, Android yourapp://path No None No
Universal Link Apple iOS only https://yourdomain.com/path Yes apple-app-site-association Yes (Safari)
App Link Google Android only https://yourdomain.com/path Yes assetlinks.json Yes (Chrome / system browser)

The simplest way to remember it: "Universal Link" is Apple. "App Link" is Google. "Deep link" is the older custom-scheme mechanism that both platforms still support but neither recommends as primary.

why the terminology matters in production

The three mechanisms have different failure modes. Mixing terminology means specs underspecify behavior.

Scenario: PM writes "add a deep link from the email to the product page." Engineer implements yourapp://product/123. Email client strips the non-HTTPS link. Link silently disappears.

Scenario: PM writes "use Universal Links on Android." Engineer is confused; Universal Links don't exist on Android. App Links do.

Scenario: PM writes "make the link work even if the app isn't installed." Engineer implements a custom scheme. Cold-install path fails with system error dialog. Engineer adds a web fallback URL, but the custom-scheme URL is what's stored in the database, not the HTTPS URL. The HTTPS URL never reaches the surface where it'd be tapped.

In each case the production bug traces back to terminological imprecision in the spec.

Apple's developer documentation: Supporting Universal Links in Your App.

// AppDelegate.swift
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }
    return handle(url)
}

And the AASA file at https://yourdomain.com/.well-known/apple-app-site-association:

{
  "applinks": {
    "details": [{
      "appIDs": ["TEAMID.com.yourcompany.yourapp"],
      "components": [{ "/": "/product/*" }]
    }]
  }
}

Full setup: /guides/ios-universal-links-setup. AASA format details: /guides/aasa-file-explained.

Android's documentation: Verify Android App Links.


<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="yourdomain.com"
              android:pathPrefix="/product/" />
    </intent-filter>
</activity>
// MainActivity.kt
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    val uri = intent.data ?: return
    handle(uri)
}

The assetlinks.json 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": ["<your SHA-256 fingerprint>"]
  }
}]

The android:autoVerify="true" flag is what makes this an App Link rather than a plain deep link. Setup walkthrough: /guides/android-app-links-setup. File format: /guides/assetlinks-json-explained.

The legacy form, still useful for in-app routing and for cases where a verified domain isn't available.

iOS — Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
    </dict>
</array>

iOS — AppDelegate.swift:

func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    // url is e.g. yourapp://product/123
    return handle(url)
}

Android — AndroidManifest.xml:

<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="yourapp" android:host="product" />
</intent-filter>

Custom schemes work for app-internal routing and for app-to-app handoffs where you control both ends. They don't work as a primary inbound-from-web mechanism in 2026.

fallback behavior, comparison

What happens when the user taps each URL type and the app isn't installed.

URL type iOS, app not installed Android, app not installed
yourapp://product/123 System dialog "Cannot open" or silent failure System dialog "No app to handle" or silent failure
https://yourdomain.com/product/123 (Universal Link) Safari opens, renders the web page Chrome opens, renders the web page
https://yourdomain.com/product/123 (App Link) (Universal Link behavior if also set up there) Chrome opens, renders the web page
Android intent URL (custom scheme) Chrome iOS treats as web URL → 404 Chrome Android tries to open the app, falls back to Play Store via S.browser_fallback_url if specified

The Android intent URL pattern is a distinct mechanism worth knowing about for cases where App Links aren't verified: /guides/intent-url-android.

Firebase Dynamic Links was a cross-platform layer that masked the iOS / Android distinction. A single FDL URL would resolve to a Universal Link on iOS and an App Link on Android underneath. FDL also handled deferred deep linking — preserving the original destination across a cold app install.

FDL was deprecated October 2023 and shut down August 25, 2025. Teams using FDL had to migrate to the underlying primitives directly, or to a paid SaaS replacement. The migration playbook: /guides/firebase-dynamic-links-migration. Strategic decision-framework: /guides/firebase-dynamic-links-replacement.

The deferred-deep-linking job specifically — preserving destination across cold install — is the hardest piece to replicate without FDL: /guides/deferred-deep-linking-explained.

what to use, when

Decision rule for 2026 deep linking:

  1. For inbound web → app routing: Universal Links on iOS, App Links on Android. The HTTPS-verified primitives are the supported path.
  2. For app-internal routing (push notification deep-link payloads, in-app menu items): custom schemes are fine. Cheap and simple.
  3. For cross-app handoffs (e.g., your app opening another app for an OAuth flow): custom schemes or ASWebAuthenticationSession on iOS, intent URLs on Android.
  4. For deferred deep linking on cold install: requires a SaaS layer (Branch, AppsFlyer, Adjust) or DIY install-referrer plumbing. The native primitives don't carry the routing payload across install.

The single biggest "deep link" failure in production is using a custom scheme where an HTTPS URL with platform association would have worked. Cost: invisible failures in email clients, web rendering, and content-share processors.

what about the iOS in-app browser case

Even Universal Links don't trigger inside third-party in-app browsers like TikTok's, Instagram's, or Threads's WKWebView instances. Apple's spec is explicit: Universal Links are intercepted only when the link is tapped from outside a WKWebView that's already running. Inside a WKWebView, the host app's webview consumes the navigation and either renders it as a web page or refuses to navigate at all.

This is the gap the in-app browser thesis addresses on the consumer side. For the engineering breakdown of why this happens: /guides/in-app-browser-cookies-explained.

If you're evaluating non-app link infrastructure where the iOS / Android distinction doesn't apply because you don't have a mobile app to route to, the no-SDK linkboo path lives at link.boo/api.

references

  • Apple Universal Links documentation
  • Google Android App Links documentation
  • Google Digital Asset Links protocol
  • Firebase Dynamic Links deprecation FAQ

Stop losing the click after the tap.

linkboo escapes the in-app browser so your real page loads — fast.

Start for free →