Universal Links vs. Deep Links: When and how to use each

Contents
Introduction
Deep Links and Apple's Universal Links both send users directly into specific content inside an app, but they work quite differently under the hood.
It's important to note that Universal Links are Apple-specific. If you want to learn more about the Android counterpart, App Links, check out this other post I wrote.
Deep Links
A Deep Link is a URL that, when clicked, opens an app to a specific screen or functionality, bypassing the need to navigate through the app's main screens. In iOS, Deep Links are often implemented using custom URL schemes, like myapp://user/999
, where myapp
is a custom scheme defined in the app. If the app is installed and registered with this scheme, iOS will open the app directly when this URL is triggered. If it's not installed, the OS simply can't resolve that URI, so nothing happens (or you'll see an error page/dialog such as "Cannot Open Page" on iOS).
Configuration
To configure Deep Links on iOS, we need to:
-
Declare a URL Scheme in
Info.plist
- Open the project's Info.plist.
- Add a new URL types entry (
CFBundleURLTypes
) as an array. - Inside it, add a dictionary with:
- Identifier (
CFBundleURLName
): e.g.com.mycompany.myapp
- URL Schemes (
CFBundleURLSchemes
): an array containing the custom scheme, e.g.myapp
- Identifier (
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>com.mycompany.myapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>
-
Implement URL handling in the app
-
Pre-iOS 13 (
AppDelegate
)func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // Parse `url.scheme`, `url.host`, `url.path` or `url.queryItems` // Route to the appropriate view controller return true }
-
iOS 13+ (
SceneDelegate
)func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { guard let context = URLContexts.first else { return } let url = context.url // Handle the URL just like in AppDelegate }
-
-
Parse and route
Break down the URL into components:
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) let host = components?.host // e.g. "user" let id = components?.queryItems?.first(where: { $0.name == "id" })?.value
Then, instantiate or navigate to the appropriate view controller based on those values.
-
Test the Deep Links
Type the URL into Safari on a device:
myapp://profile/john_doe
Or run the following line in the terminal of the simulator (or device):
xcrun simctl openurl booted "myapp://user?id=999"
-
Handle edge cases
- App not installed: Tapping a custom‑scheme link does nothing, so consider pairing with a web‑based redirect page for a fallback.
- Multiple schemes: We can register more than one under
CFBundleURLSchemes
if the app needs to handle several entry points.
With these steps, the app will recognize and respond to myapp://…
URLs.
Fallback strategies
To handle errors gracefully in production, we can implement any of the following fallback strategies:
-
Smart redirect pages
We can implement this doing the following:
- Link users to an HTTP(S) URL first (e.g.
https://mywebsite.com/open?link=myapp://foo
). - On that page, run a brief script that attempts to open the app via the custom scheme.
- After a short timeout (e.g. 1 – 2 seconds), if the app hasn't opened, automatically redirect to the App Store or Play Store listing.
- Link users to an HTTP(S) URL first (e.g.
-
Universal Links instead
By using Universal Links (iOS), taps on an HTTPS URL will either launch the app (if installed) or seamlessly load your website. No wasted taps and no manual fallback logic needed.
-
"Open in app" banners
Some sites detect mobile browsers and show a banner ("Open this content in our app for a better experience"), with a direct link to the store if the app isn't present.
Universal Links
A Universal Link is an Apple-specific method introduced in iOS 9 that allows apps to handle standard HTTPS URLs rather than custom URL schemes. A Universal Link opens an app if it's installed or directs users to a website if the app isn't installed. For instance, if we have the app installed, https://mywebsite.com/user/999
could open a specific product page in your app; if you don't have the app installed, it would direct you to the website.
Configuration
To configure Universal Links on iOS, we need to make some changes in both the app and the backend of the website:
Prerequisites
- HTTPS web domain you control (no redirects).
- iOS 9+ deployment target.
- App ID with Associated Domains enabled in your Apple Developer account.
- Xcode project with capabilities access.
App-side setup
-
Enable associated domains
-
In Xcode, select Target → Signing & Capabilities.
-
Click "+ Capability" and add Associated Domains.
-
Under the new section, add an entry for each domain:
applinks:mywebsite.com applinks:www.mywebsite.com
You can include multiple domains; iOS will check each for a valid AASA file.
-
-
Update the app's entitlements
Xcode will automatically update your
*.entitlements
file. Under the hood it now contains:<key>com.apple.developer.associated-domains</key> <array> <string>applinks:mywebsite.com</string> </array>
Server-side setup
-
Create the
apple-app-site-association
(AASA) fileAt the root of your HTTPS server (or in
/.well-known/
), host a file calledapple-app-site-association
with no extension:{ "applinks": { "details": [ { "appID": "ABCDE12345.com.yourcompany.yourapp", "paths": [ "/", "/products/*", "/blog/*", "/user/profile" ] } ] } }
Where:
- appID =
${TeamID}.${BundleIdentifier}
(e.g.ABCDE12345.com.yourcompany.yourapp
). - paths = which URL paths your app handles. Use:
"/"
for the home page."/products/*"
to match all sub‐paths."*"
to match everything on that domain."NOT /secret/*"
to explicitly exclude.
- appID =
-
Serve over HTTPS
The file must be served with
application/json
orapplication/pkc7-mime
Content-Type with no redirects, as iOS fetches the file directly.
Handling Universal Links in the app
Depending on your project setup, implement one of:
-
iOS 13+ (
SceneDelegate
)func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { guard let url = URLContexts.first?.url else { return } handleUniversalLink(url) } private func handleUniversalLink(_ url: URL) { // Parse URL.pathComponents or queryItems // e.g., if url.path.starts(with: "/products/") … print("Opened via Universal Link:", url) // Route to the correct view controller… }
-
iOS 9-12 (
AppDelegate
)func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } handleUniversalLink(url) return true }
Test the setup
-
Build and run the app on a real device (Universal Links don't work in the simulator).
-
Send yourself an iMessage or email with an
https://mywebsite.com/user/999
link. -
Tap the link:
- If the app is installed → opens your app and calls your handler.
- If not → opens Safari to the same URL on your website.
-
Inspect device logs in Console.app filtering for
swcd
andAASA
, to verify AASA fetch:swcd[xxxxx]: Finished fetching AASA file: https://mywebsite.com/apple-app-site-association
Troubleshooting
Symptom | Possible Cause | Fix |
---|---|---|
Link always opens in Safari, never in app | AASA file missing, malformed, or wrong domain | Check JSON validity & Content‑Type |
Xcode entitlements don't list your domain | Associated Domains capability not added | Re-add under Signing & Capabilities |
Partial path matching not working | Paths in AASA too restrictive or syntax issue | Verify your paths patterns |
By following these steps, you can seamlessly route HTTPS links into your app and provide a smooth user experience.
Which one to use?
-
Use Deep Links when...
-
You don't control a web domain
If you just need two apps you own to talk to each other (or a single app to open itself), and there's no website fallback, a custom URL scheme (e.g. myapp://…) is the simplest choice.
-
You need wide, old‑OS support
Custom schemes work all the way back to iOS 2 and Android 1.6. If you're targeting legacy devices (pre‑iOS 9/Android 6), schemes are your only option.
-
You want swifter in‑app routing
There's no network fetch or cryptographic check—tapping a
myapp://
link fires immediately. For some ultra‑fast, app‑only flows (e.g. internal tooling), that may matter. -
You don't need a web fallback
If falling back to a web page isn't critical (or if you don't have one), schemes let you skip the extra server setup.
-
-
Use Automatic Links when...
-
You want a seamless web‑to‑app experience
Tapping an
https://…
link opens your app if installed, or your website otherwise. No dialogs, no extra user steps. -
You need robust security
Apple verify you own the domain via an AASA (
apple-app-site-association
) orassetlinks.json
file. That prevents other apps from hijacking your URLs. -
Your link may be shared anywhere
Universal Links work in Messages, Mail, Safari, Spotlight, Siri, and third‑party apps—just like any other HTTPS link.
-
You want analytics & fallback control
Since it's a "real" URL, we can track clicks with standard web analytics and even A/B‑test redirects on the web side before handing off to the app.
-
Quick decision guide
Need / Constraint | Deep Links | Universal Links |
---|---|---|
OS support back to iOS 8. | ✅ | ❌ |
No web domain or web fallback | ✅ | ❌ |
Seamless “tap to open” in Messages, Safari, Siri, etc. | ❌ | ✅ |
Domain‑verified security guardrails | ❌ | ✅ |
Web analytics on every click | ❌ | ✅ |
Minimal setup—just an app entitlement | ✅ | Requires server AASA/assetlinks |
Bottom line
- If you just need quick, internal, or legacy-device linking with no website, go with Deep Links.
- If you have (or can create) a supporting HTTPS domain and want the most polished, secure, cross‑platform UX, go with Universal Links.