Skip to content
Tolinku
Tolinku
Sign In Start Free
iOS Development · · 8 min read

Universal Links vs Custom URL Schemes: A Comparison

By Tolinku Staff
|
Tolinku universal links dashboard screenshot for ios blog posts

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.

This article walks through exactly how each works, where each falls short, and how to move from one to the other if you're migrating an existing integration. For a comprehensive introduction to Universal Links, see our Universal Links guide, and for a detailed look at custom schemes, see the custom URL schemes guide.

How Custom URL Schemes Work

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.

You register a scheme in your app's Info.plist under CFBundleURLTypes:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
  </dict>
</array>

With that in place, any link like myapp://product/42 will open your app. In your AppDelegate or SceneDelegate, you handle incoming URLs like this:

// AppDelegate
func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
    guard url.scheme == "myapp" else { return false }

    // Parse the path and route accordingly
    let path = url.host ?? ""
    let components = URLComponents(url: url, resolvingAgainstBaseURL: false)

    switch path {
    case "product":
        let productId = components?.queryItems?.first(where: { $0.name == "id" })?.value
        navigateToProduct(id: productId)
        return true
    case "profile":
        let userId = components?.queryItems?.first(where: { $0.name == "userId" })?.value
        navigateToProfile(userId: userId)
        return true
    default:
        return false
    }
}

The scheme is declared entirely client-side. iOS trusts the app's own declaration without any external verification. That simplicity is both the main advantage and the central problem.

The Core Problem With Custom Schemes

Because any app can claim any scheme, there is nothing stopping a malicious app from registering myapp:// 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 deep linking security.

When your app is not installed, tapping a myapp:// 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).

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.

Universal Links use standard HTTPS URLs. The key difference is that Apple'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.

The AASA File

You host the AASA file at https://yourdomain.com/.well-known/apple-app-site-association (no file extension). Here is a typical structure:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.example.myapp"],
        "components": [
          {
            "/": "/product/*",
            "comment": "Match any product detail URL"
          },
          {
            "/": "/profile/*",
            "comment": "Match user profile URLs"
          },
          {
            "/": "/invite/*",
            "comment": "Match invitation links"
          }
        ]
      }
    ]
  }
}

The appIDs field combines your 10-character Apple Team ID with your app'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 Content-Type of application/json.

Universal Links arrive through the NSUserActivity API, not through the openURL delegate method. In a SceneDelegate:

func scene(
    _ scene: UIScene,
    continue userActivity: NSUserActivity
) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let incomingURL = userActivity.webpageURL else {
        return
    }

    handleIncomingURL(incomingURL)
}

private func handleIncomingURL(_ url: URL) {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
    let pathComponents = url.pathComponents

    guard pathComponents.count >= 2 else { return }

    switch pathComponents[1] {
    case "product":
        let productId = components?.queryItems?.first(where: { $0.name == "id" })?.value
        navigateToProduct(id: productId)
    case "profile":
        let userId = pathComponents.count > 2 ? pathComponents[2] : nil
        navigateToProfile(userId: userId)
    case "invite":
        let token = pathComponents.count > 2 ? pathComponents[2] : nil
        handleInvitation(token: token)
    default:
        break
    }
}

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.

Side-by-Side Comparison

Factor Custom URL Schemes Universal Links
Security No verification; any app can claim the same scheme Domain-verified; Apple checks AASA file at install time
Uniqueness guarantee None Tied to your domain ownership
Fallback when app not installed Dead end (browser error) Falls back to the HTTPS URL in Safari
User experience Visible non-standard URL, possible redirect prompts Standard HTTPS URL, seamless handoff
Setup complexity Low (Info.plist only) Moderate (AASA file + entitlements + server hosting)
App-to-app communication Well-suited; standard pattern Not designed for it; use schemes or XPC instead
OAuth callback support Standard and widely supported Possible but less common; most SDKs still default to schemes
Search engine indexing Not indexable HTTPS URLs are indexable if you configure web content
Works when app is installed Yes Yes
iOS version requirement All versions iOS 9+ (AASA v2 with components requires iOS 13+)

When Custom Schemes Still Make Sense

Custom URL schemes are not obsolete. There are specific scenarios where they remain the right tool.

Inter-app communication. 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.

OAuth callback URLs. 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 myapp://oauth/callback?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.

Local development and testing. 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.

Closed enterprise environments. 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.

For any public-facing link, Universal Links are the correct choice.

Marketing and growth campaigns. 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.

Shared content links. 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.

App Store review. Apple'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.

Referral and attribution flows. 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 Tolinku, handle the full cycle: the pre-install click, the attribution, the install, and the post-install routing to the right screen.

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.

Step 1: Set up AASA hosting. Our AASA file setup guide walks through the full hosting requirements. Host the AASA file on your domain before you ship any app update. Apple's CDN fetches it at install time, so the file needs to exist before devices start installing your updated app.

Step 2: Add the Associated Domains entitlement. In Xcode, go to your target's Signing and Capabilities tab. Add the Associated Domains capability and add entries for each domain:

applinks:yourdomain.com
applinks:www.yourdomain.com

Step 3: Implement the NSUserActivity handler. Add the Universal Link handler to your SceneDelegate as shown above. Keep your existing openURL handler in place during the transition so that existing custom scheme links still work.

Step 4: Update your link generation. Any place your app or backend generates shareable links, switch the output from myapp:// to https://yourdomain.com/. This includes share sheets, invitation flows, password reset emails, and notification payloads.

Step 5: Validate before shipping. Use Apple's AASA Validator tool (note: search for "Apple AASA validator" 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.

Step 6: Keep the custom scheme for OAuth callbacks. 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.

For a complete walkthrough of what goes into a production AASA setup, the Tolinku Universal Links documentation covers the edge cases including CDN caching, multi-app AASA files, and the mode parameter introduced in iOS 13.

Which to Use

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.

Keep custom schemes for OAuth callbacks, inter-app communication contracts with known partner apps, and development tooling where server infrastructure is inconvenient.

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.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.