{"id":463,"date":"2026-03-16T17:00:00","date_gmt":"2026-03-16T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=463"},"modified":"2026-03-07T04:35:59","modified_gmt":"2026-03-07T09:35:59","slug":"universal-links-vs-custom-schemes","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/universal-links-vs-custom-schemes\/","title":{"rendered":"Universal Links vs Custom URL Schemes: A Comparison"},"content":{"rendered":"\n<p>iOS deep linking comes down to two mechanisms: custom URL schemes and Universal Links. They both open your app from a URL, but the similarities stop there. Under the hood, they use completely different OS-level machinery, carry different security properties, and produce noticeably different experiences for users.<\/p>\n\n\n\n<p>This article walks through exactly how each works, where each falls short, and how to move from one to the other if you&#39;re migrating an existing integration. For a comprehensive introduction to Universal Links, see our <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">Universal Links guide<\/a>, and for a detailed look at custom schemes, see the <a href=\"https:\/\/tolinku.com\/blog\/custom-url-schemes-guide\/\">custom URL schemes guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Custom URL Schemes Work<\/h2>\n\n\n\n<p>Custom URL schemes let you register a private URL prefix with iOS. When any link starting with that prefix is tapped anywhere on the device, iOS routes it to your app. No verification required, no server involved.<\/p>\n\n\n\n<p>You register a scheme in your app&#39;s <code>Info.plist<\/code> under <code>CFBundleURLTypes<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;key&gt;CFBundleURLTypes&lt;\/key&gt;\n&lt;array&gt;\n  &lt;dict&gt;\n    &lt;key&gt;CFBundleURLSchemes&lt;\/key&gt;\n    &lt;array&gt;\n      &lt;string&gt;myapp&lt;\/string&gt;\n    &lt;\/array&gt;\n    &lt;key&gt;CFBundleURLName&lt;\/key&gt;\n    &lt;string&gt;com.example.myapp&lt;\/string&gt;\n  &lt;\/dict&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n\n\n\n<p>With that in place, any link like <code>myapp:\/\/product\/42<\/code> will open your app. In your <code>AppDelegate<\/code> or <code>SceneDelegate<\/code>, you handle incoming URLs like this:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ AppDelegate\nfunc application(\n    _ app: UIApplication,\n    open url: URL,\n    options: [UIApplication.OpenURLOptionsKey: Any] = [:]\n) -&gt; Bool {\n    guard url.scheme == &quot;myapp&quot; else { return false }\n\n    \/\/ Parse the path and route accordingly\n    let path = url.host ?? &quot;&quot;\n    let components = URLComponents(url: url, resolvingAgainstBaseURL: false)\n\n    switch path {\n    case &quot;product&quot;:\n        let productId = components?.queryItems?.first(where: { $0.name == &quot;id&quot; })?.value\n        navigateToProduct(id: productId)\n        return true\n    case &quot;profile&quot;:\n        let userId = components?.queryItems?.first(where: { $0.name == &quot;userId&quot; })?.value\n        navigateToProfile(userId: userId)\n        return true\n    default:\n        return false\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The scheme is declared entirely client-side. iOS trusts the app&#39;s own declaration without any external verification. That simplicity is both the main advantage and the central problem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Core Problem With Custom Schemes<\/h3>\n\n\n\n<p>Because any app can claim any scheme, there is nothing stopping a malicious app from registering <code>myapp:\/\/<\/code> before your app does (or alongside it). When multiple apps register the same scheme, iOS picks one and the behavior is undefined. This is the URI scheme hijacking problem, and it is not theoretical. It has been exploited in practice against OAuth flows. For a full breakdown of these risks, see our article on <a href=\"https:\/\/tolinku.com\/blog\/deep-linking-security\/\">deep linking security<\/a>.<\/p>\n\n\n\n<p>When your app is not installed, tapping a <code>myapp:\/\/<\/code> link produces a dead end. Safari displays an error. There is no fallback to a web page or app store listing unless you build one yourself using JavaScript in a web page, which is fragile and breaks in many contexts (iMessage, Notes, most third-party apps).<\/p>\n\n\n\n<p>Custom schemes also do not work in web contexts where JavaScript is blocked or in app clips, and they cannot be indexed by search engines in any meaningful way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Universal Links Work<\/h2>\n\n\n\n<p>Universal Links use standard HTTPS URLs. The key difference is that Apple&#39;s infrastructure verifies your ownership of the domain before iOS will route those URLs to your app. This verification happens through a JSON file called the Apple App Site Association (AASA) file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The AASA File<\/h3>\n\n\n\n<p>You host the AASA file at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code> (no file extension). Here is a typical structure:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;TEAMID.com.example.myapp&quot;],\n        &quot;components&quot;: [\n          {\n            &quot;\/&quot;: &quot;\/product\/*&quot;,\n            &quot;comment&quot;: &quot;Match any product detail URL&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/profile\/*&quot;,\n            &quot;comment&quot;: &quot;Match user profile URLs&quot;\n          },\n          {\n            &quot;\/&quot;: &quot;\/invite\/*&quot;,\n            &quot;comment&quot;: &quot;Match invitation links&quot;\n          }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>appIDs<\/code> field combines your 10-character Apple Team ID with your app&#39;s bundle identifier. Apple fetches this file via its own CDN infrastructure at app install time and periodically thereafter. The file must be served over HTTPS with a valid certificate and return a <code>Content-Type<\/code> of <code>application\/json<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Handling Universal Links in Swift<\/h3>\n\n\n\n<p>Universal Links arrive through the <code>NSUserActivity<\/code> API, not through the <code>openURL<\/code> delegate method. In a <code>SceneDelegate<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-swift\">func scene(\n    _ scene: UIScene,\n    continue userActivity: NSUserActivity\n) {\n    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,\n          let incomingURL = userActivity.webpageURL else {\n        return\n    }\n\n    handleIncomingURL(incomingURL)\n}\n\nprivate func handleIncomingURL(_ url: URL) {\n    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)\n    let pathComponents = url.pathComponents\n\n    guard pathComponents.count &gt;= 2 else { return }\n\n    switch pathComponents[1] {\n    case &quot;product&quot;:\n        let productId = components?.queryItems?.first(where: { $0.name == &quot;id&quot; })?.value\n        navigateToProduct(id: productId)\n    case &quot;profile&quot;:\n        let userId = pathComponents.count &gt; 2 ? pathComponents[2] : nil\n        navigateToProfile(userId: userId)\n    case &quot;invite&quot;:\n        let token = pathComponents.count &gt; 2 ? pathComponents[2] : nil\n        handleInvitation(token: token)\n    default:\n        break\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>If your app is not installed when the link is tapped, iOS falls back to Safari and loads the URL like a normal web page. This means your website can serve an App Store redirect, a web fallback page, or both, with no dead ends.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Side-by-Side Comparison<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Factor<\/th>\n<th>Custom URL Schemes<\/th>\n<th>Universal Links<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Security<\/td>\n<td>No verification; any app can claim the same scheme<\/td>\n<td>Domain-verified; Apple checks AASA file at install time<\/td>\n<\/tr>\n<tr>\n<td>Uniqueness guarantee<\/td>\n<td>None<\/td>\n<td>Tied to your domain ownership<\/td>\n<\/tr>\n<tr>\n<td>Fallback when app not installed<\/td>\n<td>Dead end (browser error)<\/td>\n<td>Falls back to the HTTPS URL in Safari<\/td>\n<\/tr>\n<tr>\n<td>User experience<\/td>\n<td>Visible non-standard URL, possible redirect prompts<\/td>\n<td>Standard HTTPS URL, seamless handoff<\/td>\n<\/tr>\n<tr>\n<td>Setup complexity<\/td>\n<td>Low (Info.plist only)<\/td>\n<td>Moderate (AASA file + entitlements + server hosting)<\/td>\n<\/tr>\n<tr>\n<td>App-to-app communication<\/td>\n<td>Well-suited; standard pattern<\/td>\n<td>Not designed for it; use schemes or XPC instead<\/td>\n<\/tr>\n<tr>\n<td>OAuth callback support<\/td>\n<td>Standard and widely supported<\/td>\n<td>Possible but less common; most SDKs still default to schemes<\/td>\n<\/tr>\n<tr>\n<td>Search engine indexing<\/td>\n<td>Not indexable<\/td>\n<td>HTTPS URLs are indexable if you configure web content<\/td>\n<\/tr>\n<tr>\n<td>Works when app is installed<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>iOS version requirement<\/td>\n<td>All versions<\/td>\n<td>iOS 9+ (AASA v2 with <code>components<\/code> requires iOS 13+)<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">When Custom Schemes Still Make Sense<\/h2>\n\n\n\n<p>Custom URL schemes are not obsolete. There are specific scenarios where they remain the right tool.<\/p>\n\n\n\n<p><strong>Inter-app communication.<\/strong> When your app needs to launch another app or accept launches from a known partner app on the same device, custom schemes are the standard mechanism. The scheme is effectively an agreed-upon contract between two known apps. This pattern is used by keyboard extensions, share extensions, and integrations between apps in the same suite.<\/p>\n\n\n\n<p><strong>OAuth callback URLs.<\/strong> Most OAuth providers and their iOS SDKs use custom schemes for the redirect URI after authentication. The flow looks like: your app opens a browser for the auth provider, the user authenticates, the provider redirects to <code>myapp:\/\/oauth\/callback?code=...<\/code>, and iOS routes that back to your app. While Universal Links can technically serve this role, the OAuth community (and most SDK implementations) standardized on custom schemes for this pattern years ago. Changing it creates compatibility headaches without much benefit.<\/p>\n\n\n\n<p><strong>Local development and testing.<\/strong> Custom schemes work without a live server and without a valid TLS certificate. During early development, before you have your AASA infrastructure in place, schemes let you test deep link routing logic quickly. Just make sure you do not ship your final product with schemes as the only deep link mechanism.<\/p>\n\n\n\n<p><strong>Closed enterprise environments.<\/strong> If your app is distributed internally via MDM and links only ever appear inside controlled tools (email clients, internal wikis), the security arguments against custom schemes matter less. The hijacking risk is reduced when device management controls what apps are installed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When Universal Links Are Required<\/h2>\n\n\n\n<p>For any public-facing link, Universal Links are the correct choice.<\/p>\n\n\n\n<p><strong>Marketing and growth campaigns.<\/strong> Links shared in email campaigns, social media posts, SMS, and QR codes will be tapped on devices where your app may or may not be installed. Universal Links degrade gracefully to a web page. Custom schemes produce an error. The difference in conversion rate is measurable.<\/p>\n\n\n\n<p><strong>Shared content links.<\/strong> When users share content from within your app (a product, an article, a profile), those links go into messages, posts, and emails. Recipients may be on iOS, Android, or desktop. An HTTPS Universal Link works in all three contexts. A custom scheme works in none of the non-iOS contexts and fails on iOS when the app is not installed.<\/p>\n\n\n\n<p><strong>App Store review.<\/strong> Apple&#39;s guidelines do not prohibit custom schemes, but reviewers have rejected apps that provide a broken experience when a scheme link fails. Using Universal Links for user-facing flows avoids this category of rejection entirely.<\/p>\n\n\n\n<p><strong>Referral and attribution flows.<\/strong> Deferred deep linking (routing a user to specific in-app content after they install the app from the App Store) requires a web-based link as the entry point. Custom schemes cannot serve as deferred deep link entry points because there is nothing to tap before the app is installed. Universal Links, combined with a deep linking platform like <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a>, handle the full cycle: the pre-install click, the attribution, the install, and the post-install routing to the right screen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Migrating From Custom Schemes to Universal Links<\/h2>\n\n\n\n<p>If you have an existing app that uses custom schemes and you want to move to Universal Links, the process has a few distinct stages.<\/p>\n\n\n\n<p><strong>Step 1: Set up AASA hosting.<\/strong> Our <a href=\"https:\/\/tolinku.com\/blog\/aasa-file-setup\/\">AASA file setup guide<\/a> walks through the full hosting requirements. Host the AASA file on your domain before you ship any app update. Apple&#39;s CDN fetches it at install time, so the file needs to exist before devices start installing your updated app.<\/p>\n\n\n\n<p><strong>Step 2: Add the Associated Domains entitlement.<\/strong> In Xcode, go to your target&#39;s Signing and Capabilities tab. Add the Associated Domains capability and add entries for each domain:<\/p>\n\n\n\n<pre><code>applinks:yourdomain.com\napplinks:www.yourdomain.com\n<\/code><\/pre>\n\n\n\n<p><strong>Step 3: Implement the <code>NSUserActivity<\/code> handler.<\/strong> Add the Universal Link handler to your <code>SceneDelegate<\/code> as shown above. Keep your existing <code>openURL<\/code> handler in place during the transition so that existing custom scheme links still work.<\/p>\n\n\n\n<p><strong>Step 4: Update your link generation.<\/strong> Any place your app or backend generates shareable links, switch the output from <code>myapp:\/\/<\/code> to <code>https:\/\/yourdomain.com\/<\/code>. This includes share sheets, invitation flows, password reset emails, and notification payloads.<\/p>\n\n\n\n<p><strong>Step 5: Validate before shipping.<\/strong> Use Apple&#39;s <a href=\"https:\/\/branch.com\" rel=\"nofollow noopener\" target=\"_blank\">AASA Validator<\/a> tool (note: search for &quot;Apple AASA validator&quot; to find Apple-maintained or community tools). You can also test locally by installing a development build via Xcode, tapping a Universal Link in Notes or iMessage, and confirming your app opens. Safari will not trigger Universal Links if you type or paste directly into the address bar; use the Notes app or Messages.<\/p>\n\n\n\n<p><strong>Step 6: Keep the custom scheme for OAuth callbacks.<\/strong> If you use OAuth in your app, keep the scheme registration for the callback URL. Replace only the user-facing, shareable links with Universal Links.<\/p>\n\n\n\n<p>For a complete walkthrough of what goes into a production AASA setup, the <a href=\"https:\/\/tolinku.com\/docs\/concepts\/universal-links\/\">Tolinku Universal Links documentation<\/a> covers the edge cases including CDN caching, multi-app AASA files, and the <code>mode<\/code> parameter introduced in iOS 13.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Which to Use<\/h2>\n\n\n\n<p>Use Universal Links for every link that leaves your app and enters the world: marketing emails, social shares, QR codes, SMS campaigns, push notification payloads that users might forward, and any URL you display publicly. They are more work to set up but the security, reliability, and user experience advantages are substantial.<\/p>\n\n\n\n<p>Keep custom schemes for OAuth callbacks, inter-app communication contracts with known partner apps, and development tooling where server infrastructure is inconvenient.<\/p>\n\n\n\n<p>The two mechanisms are not mutually exclusive. Most production iOS apps register both, using each for the scenario it handles best. The key is knowing which use case belongs to which tool.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Compare Universal Links and custom URL schemes for iOS deep linking. Understand security, reliability, and user experience differences.<\/p>\n","protected":false},"author":2,"featured_media":462,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Universal Links vs Custom URL Schemes: A Comparison","rank_math_description":"Compare Universal Links and custom URL schemes for iOS deep linking. Understand security, reliability, and user experience differences.","rank_math_focus_keyword":"universal links vs custom schemes","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-vs-custom-schemes.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-universal-links-vs-custom-schemes.png","footnotes":""},"categories":[12],"tags":[20,24,31,22],"class_list":["post-463","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ios","tag-deep-linking","tag-ios","tag-swift","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/463","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=463"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/463\/revisions"}],"predecessor-version":[{"id":2786,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/463\/revisions\/2786"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/462"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=463"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=463"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=463"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}