On this page
- scope
- one-paragraph summary
- the table
- why the terminology matters in production
- iOS Universal Link — code
- Android App Link — code
- custom-scheme deep link — code
- fallback behavior, comparison
- the deep-link-from-Firebase legacy
- what to use, when
- what about the iOS in-app browser case
- related dev reading
- references
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 | 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.
iOS Universal Link — code
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 App Link — code
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.
custom-scheme deep link — code
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.
the deep-link-from-Firebase legacy
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:
- For inbound web → app routing: Universal Links on iOS, App Links on Android. The HTTPS-verified primitives are the supported path.
- For app-internal routing (push notification deep-link payloads, in-app menu items): custom schemes are fine. Cheap and simple.
- For cross-app handoffs (e.g., your app opening another app for an OAuth flow): custom schemes or
ASWebAuthenticationSessionon iOS, intent URLs on Android. - 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.
related dev reading
- Pair: /guides/universal-links-vs-deep-links
- Setup walkthroughs: /guides/ios-universal-links-setup, /guides/android-app-links-setup
- File formats: /guides/aasa-file-explained, /guides/assetlinks-json-explained
- Android intent URL: /guides/intent-url-android
- Deferred deep link: /guides/deferred-deep-linking-explained
- Cluster C hub: /guides/firebase-dynamic-links-replacement
- Bridge: /guides/in-app-browser-logged-out
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