Home / Blog / iOS deep linking in 2025: URL schemes, Universal Link, App Clip

iOS deep linking in 2025: URL schemes, Universal Link, App Clip

Deep linking on iOS has had three generations: URL scheme, Universal Link, App Clip. Which one when, and how to set them up.

When someone says “deep linking” on iOS, they could mean one of three things: custom URL scheme, Universal Link, or App Clip. Each was designed for a different era, each solves a different problem.

I’ve shipped all three in production over the last decade. Here’s when to pick which.

Custom URL Scheme (old, still used)

The original approach, around since the very first iOS. You register a URL scheme in Info.plist:

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

Now myapp://user/123 opens your app. Simple.

Pros:
– Minimal setup
– No server-side work
– Great for app-to-app communication (app X opens app Y)

Cons:
Not secure: another app can register the same scheme. Malware could claim your banking app’s scheme.
Unreliable: on a scheme conflict iOS picks arbitrarily.
Not a web link: you can’t click one on a web page (Safari says “unknown URL”).
The legacy cousin: Apple is steadily deprecating custom URL schemes.

Where it’s still reasonable:

  1. App-to-app communication (your main app talking to your extension app)
  2. OAuth redirects (provider back to the app callback)
  3. Legacy system integrations

Don’t reach for it on a greenfield project. Universal Link exists for a reason.

Universal Link (modern, recommended)

Introduced in iOS 9 (2015). The “officially recommended” deep link approach today.

Four-step setup:

Step 1: put /.well-known/apple-app-site-association on your server:

{
    "applinks": {
        "details": [
            {
                "appIDs": ["TEAM_ID.com.example.app"],
                "components": [
                    { "/": "/user/*" },
                    { "/": "/product/*" }
                ]
            }
        ]
    }
}

JSON, served with content-type application/json.

Step 2: add the Associated Domains entitlement in Xcode Signing & Capabilities:

applinks:example.com

Step 3: handle the URL in AppDelegate or the SwiftUI App lifecycle:

.onOpenURL { url in
    if url.pathComponents.contains("user") {
        let userId = url.lastPathComponent
        navigateToUser(userId)
    }
}

Step 4: test (the previous post covered that in detail).

Pros:
– Web links work: Safari, Mail, WhatsApp, anywhere
– Secure: only apps associated with your domain can catch the link
– Fallback: without the app installed, the web page opens
– SEO-friendly: regular URLs

Cons:
– Requires server setup (serve the AASA file)
– Debugging is rough (AASA caching)
– No deferred deep linking out of the box

App Clips (for specific scenarios)

Introduced in iOS 14 (2020). App functionality without the app.

It’s a special variant of Universal Link. When the URL is opened and the app isn’t installed, the App Clip downloads and runs (instead of the full app).

The use case: a user at a specific physical place wants to do a specific thing (payment, rental). They’ve never installed your app. The App Clip opens in five seconds and they’re done.

When to use it: I covered this in depth in the App Clips post. Short version: offline payment, event entry, scooter rental, and similar.

The decision tree

For deep linking on a new iOS project:

  1. Will a web page link into the app? Universal Link.
  2. An OAuth callback or other one-off redirect? Custom URL scheme.
  3. Physical interaction (NFC, QR) plus a one-shot task? App Clip.
  4. Native-to-native (another of your apps) communication? Custom URL scheme.
  5. Standard engagement from a link? Universal Link.

Default to Universal Link. The others are exceptions.

Designing the link structure

How should you shape your deep link URLs? A rule:

Good: https://example.com/user/123
– Meaningful on the web (user profile page)
– Easy to parse on the app side
– Backwards-compatible if you start versioning

Bad: https://example.com/?action=openUser&id=123
– No path on the web deployment
– Query params aren’t concise or semantic
– Tracking is harder in analytics

Path hierarchy:

/user/123                  # User profile
/user/123/posts            # User's posts
/user/123/posts/456        # Specific post
/product/xyz               # Product
/product/xyz/reviews       # Reviews
/checkout/cart             # Cart
/checkout/payment          # Payment flow

The same hierarchy on the web and in the app. Parsing code stays standard.

Parameter parsing

Query parameters can carry extra context:

https://example.com/product/xyz?ref=email_campaign&variant=b

On the app side, parse them:

func handle(_ url: URL) {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
    let queryItems = components?.queryItems ?? []
    
    var ref: String?
    var variant: String?
    for item in queryItems {
        switch item.name {
        case "ref": ref = item.value
        case "variant": variant = item.value
        default: break
        }
    }
    
    // Attribution tracking
    if let ref = ref {
        analytics.track("deep_link_arrival", properties: ["ref": ref])
    }
}

That’s how you attribute marketing campaigns.

Testing strategy

Development:
– Simulator or a physical device
– Universal Link developer mode (the query-string trick)
– Confirm the AASA file is being served correctly (curl it)
– Entitlements match the right provisioning profile

Staging:
– AASA served from a real domain
– Test with a TestFlight build
– Real network, not a localhost proxy

Production verification:
– Smoke test: tap links from different places (Mail, Safari, Notes, WhatsApp)
– Analytics: are deep link arrival events coming in?
– Fallback: with the app uninstalled, does the web page open?

Migration: URL scheme to Universal Link

When an older app has a custom URL scheme and you want to move to Universal Link:

  1. Coexist: both systems can run together. Keep the old URL scheme handler, add a new Universal Link handler.
  2. Gradual migration: email templates and push payloads shift to Universal Link URLs one by one.
  3. Sunset: after 6 to 12 months, deprecate the URL scheme. Users still hitting the old URLs get an “update your app” message.

That transition takes two or three sprints, but it’s worth it for security and UX.

Takeaway

Deep linking on iOS is a bigger topic than it looks. Picking the right technology (Universal Link by default, URL scheme niche), designing good URLs, testing across the matrix.

Start every new project with Universal Link. Plan migrations for existing projects. Only use App Clip for a genuine use case. Save custom URL scheme for legacy or OAuth.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch