Skip to content
iOS Universal Links: Setup, Testing and Debugging Guide

16 Minutes

iOS Universal Links: Setup, Testing and Debugging Guide

Fix Bugs Faster! Log Collection Made Easy

Get started

Debugging iOS Universal Links can be trickier than it seems on the surface. Several layers have to work perfectly, and when something fails iOS rarely gives you the reason.

So we’ve put together this guide to implement and debug iOS Universal Links correctly in production environments. We’ll get into how iOS verifies domain associations, how AASA file and app entitlements interact, how to configure and handle links in SwiftUI and UIKit, and how to validate and troubleshoot behavior on real devices.

By the end, you’ll have a rounded understanding of both the setup process and the system-level verification logic behind iOS Universal Links. But if you’ve come with some existing knowledge and want to sharpen a specific area, feel free to jump straight to the bit you need.

First, what are iOS Universal Links?

iOS Universal Links are Apple’s secure deep linking mechanism that binds a website domain to a specific app.

When a user taps a supported link:

  • If the app is installed → the link opens inside the app.
  • If the app is not installed → the link opens in Safari.
  • The same URL works in both cases.

Unlike custom URL schemes, Apple Universal Links are verified by iOS using the AASA file, which prevents link hijacking.

How Apple Universal Links work

This bit is crucial to everything that comes next.

Apple Universal Link behavior is enforced by iOS at the system level, not by the browser or the app alone.

When a user taps a supported HTTPS link, iOS performs a verification check:

  1. It verifies that the domain is associated with a specific app.
  2. It confirms that the app declares support for that domain.
  3. It checks whether the requested path is allowed.
  4. If everything matches, the app opens. If not, the link falls back to Safari.

This verification relies on two core components:

  • the apple-app-site-association file hosted on the domain.
  • the Associated Domains entitlement configured in Xcode.

Both must align exactly for Universal Links to activate.

The apple-app-site-association (AASA) File

The AASA file is a JSON file hosted on our website that tells iOS which app is allowed to open links for that domain.

It includes:

  • The app’s Team ID.
  • The app’s Bundle ID.
  • The URL paths the app is allowed to handle.

When the app is installed, iOS downloads and caches this file. From that point forward, the system uses it to decide whether a tapped link should open inside the app or stay in Safari.

💡 If the file is missing, malformed, served incorrectly or fails validation, the association does not activate.

Associated domains and app verification

Another essential point to note is that you enable the Associated Domains capability inside Xcode.

This tells iOS that the app intends to handle links for specific domains.

Inside the app’s entitlements, we declare:

applinks:example.com

When the app is installed:

  • iOS checks the declared domain.
  • It fetches the AASA file from that domain.
  • It validates the Team ID and Bundle ID.
  • It links the domain to the app at the system-level.

If the Team ID, Bundle ID, or domain declaration does not match exactly, the association fails and the link opens in Safari instead of the app.

How to set up and use iOS Universal Links

To use iOS Universal Links correctly, we must establish a verified association between the app and the domain, then handle incoming URLs inside the app.

The process is simple but precise, if that’s possible. Small configuration mistakes will bring failures.

  1. Create the AASA file: Add the correct TEAMID.BundleIdentifier and define the URL paths the app should handle.
  2. Host it correctly: Serve the file over HTTPS at the required location, without redirects, returning HTTP 200 with application/json.
  3. Enable Associated Domains: Add applinks:example.com in Xcode under Associated Domains.
  4. Handle incoming links in the app: Implement the Universal Link callback and route users based on the URL path.
  5. Test on real devices: Confirm verification, check caching behavior, and validate fallback to Safari when the app is not installed.

Requirements for iOS Universal Links

Before we go any further, it feels like a good time to run through the necessary infrastructure and credentials. This is particularly important for Universal Link verification because it depends on domain control, correct app identifiers and Xcode capabilities.

Some requirements are more important than others. These are the absolute must-note-downers:

RequirementWhy It Matters
Apple Developer accountProvides access to the Team ID and allows us to configure Associated Domains in Xcode.
Valid App ID and Bundle IdentifierMust match exactly between the app and the AASA file for verification to succeed.
Public HTTPS domain we controlUniversal Links only work on secure, publicly accessible domains.
Server access to host the AASA fileWe must upload and correctly serve the apple-app-site-association file.
Xcode project accessRequired to enable the Associated Domains capability in the app.

If any of these elements are misaligned, the association will not activate.

Step 1: Create the AASA File

This is the fun part. And actually, it’s pretty straightforward.

First create a file named exactly like this:

apple-app-site-association

Don’t add any file extension. The filename must not end with .json, .txt, or any other extension.

Then add the following structure:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID":"TEAMID.BundleIdentifier",
        "paths": ["*"]
      }
    ]
  }
}

The "apps" field is required but unused for Universal Links. It must be present as an empty array.

Replace:

  • TEAMID with our Apple Developer Team ID.
  • BundleIdentifier with our app’s Bundle Identifier.

For example:

"appID": "ABCD123456.com.bugfender.app"

For initial setup, we can use "*" in paths to allow all routes during verification.

Once everything works, we should restrict paths to only the routes the app actually handles. This reduces unintended deep linking and improves security.

The TEAMID.BundleIdentifier value must match exactly what signs the app. Even a small mismatch prevents iOS Universal Links from activating.

Apple Developer portal Membership page showing the Team ID field

Where to find the Team ID and Bundle Identifier

Before hosting the file, we should verify both values directly from their sources.

Find the Team ID

Find the Bundle Identifier

  • Open our project in Xcode
  • Select the app target
  • Go to General
  • Copy the Bundle Identifier

If we have multiple targets (dev, staging, production), we must confirm we are using the correct one. Even a single character mismatch will cause iOS Universal Links to open Safari instead of our app.

Xcode project settings showing the Bundle Identifier field under the General tab

Step 2: Host the AASA File Correctly

Now we’ve gotta upload the apple-app-site-association file to the public root of the website — the same location where the main site files are served.

Be sure you place it at one of these exact URLs:

  • https://example.com/apple-app-site-association
  • https://example.com/.well-known/apple-app-site-association

Make sure the file is uploaded:

  • As a static file.
  • Without a .json extension.
  • Inside the web root (not a private folder).

If using a framework or static hosting provider, ensure the file is served directly and not handled by application routing.

Verify server response (status code and headers)

After hosting the file, you must also confirm that iOS can fetch it exactly as expected.

The AASA file must:

  • Return HTTP 200.
  • Not redirect (no 301 or 302).
  • Be served with Content-Type: application/json .

But don’t worry. We can verify this using:

curl -I <https://example.com/apple-app-site-association>

💡 If the file redirects, returns a non-200 status, or has incorrect headers, iOS will open Safari instead of the app.

Step 3: Enable associated domains in Xcode

Universal Links are a two-way trust model: both parties have got to confirm the relationship for it to fly. iOS won’t enable a Universal Link unless both the website and the app confirm their link.

So once the AASA file is correctly hosted, we must tell iOS that the app supports links from that domain. In Xcode:

  • Open the project.
  • Select the app target.
  • Go to Signing & Capabilities.
  • Click + Capability.
  • Add Associated Domains.

Under Associated Domains, you should add the domain in this format:

applinks:example.com

A few important details to commit to memory:

  • Each domain or subdomain must be declared separately (example.com and www.example.com are different).
  • Do not include http:// or https:// in the applinks: value.
  • Universal Links only work over HTTPS.

Step 4: Handle iOS Universal Links in your app

After iOS verifies the domain, it launches the app when a supported link is tapped. Now we decide what should happen inside the app.

With the incoming URL, we can:

  • Open a specific screen (for example, a product or profile page).
  • Resume a flow (checkout, onboarding, password reset).
  • Require login first, then continue to the deep link.
  • Fall back to a safe screen if the route is unknown.

To make this work, we must:

  1. Choose where to handle the link (SwiftUI or UIKit).
  2. Capture the incoming URL from the app lifecycle.
  3. Extract the path and any query parameters.
  4. Map that information to the correct internal screen.

SwiftUI scenario: handle links with onOpenURL

In modern SwiftUI apps, we handle iOS Universal Links at the root level of the app.

  • Open the file marked with @main
  • Ensure your root view is loaded there (for example, RootView())
  • Attach an .onOpenURL handler to that root view
  • Inspect the incoming URL path
  • Update navigation state based on that path

The example below assumes RootView is used inside the @main app entry point.

import SwiftUI

struct RootView: View {

    // Stores the current navigation route
    @State private var route: String?

    var body: some View {
        NavigationStack {
            VStack {

                // Navigation triggered when route == "profile"
                NavigationLink("Go to Profile",
                               destination: Text("Profile Screen"),
                               tag: "profile",
                               selection: $route)

                // Navigation triggered when route == "settings"
                NavigationLink("Go to Settings",
                               destination: Text("Settings Screen"),
                               tag: "settings",
                               selection: $route)
            }
        }

        // Called automatically when a Universal Link opens the app
        .onOpenURL { url in
            let path = url.path

            // Example: <https://example.com/profile>
            if path == "/profile" {
                route = "profile"
            }

            // Example: <https://example.com/settings>
            else if path == "/settings" {
                route = "settings"
            }

            // Unknown route → stay on default screen
            else {
                route = nil
            }
        }
    }
}

Here we inspect url.path, update navigation state, and let SwiftUI reactively present the correct screen.

UIKit scenario: handle links with AppDelegate

In a UIKit app, we handle iOS Universal Links in the app lifecycle and then route based on the incoming URL.

  • Open AppDelegate.swift
  • Implement application(_:continue:restorationHandler:)
  • Extract the incoming URL from userActivity.webpageURL
  • Inspect url.path
  • Push the right view controller (or fall back safely)

The example below assumes we have a navigation controller available and simple view controllers like ProfileViewController and SettingsViewController.

import UIKit

func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    // Triggered when a Universal Link opens the app
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }

    let path = url.path
    print("Universal Link received:", path)

    // Example: <https://example.com/profile>
    if path == "/profile" {
        let vc = ProfileViewController()
        navigationController?.pushViewController(vc, animated: true)
        return true
    }

    // Example: <https://example.com/settings>
    if path == "/settings" {
        let vc = SettingsViewController()
        navigationController?.pushViewController(vc, animated: true)
        return true
    }

    // Unknown route → show default screen
    navigationController?.popToRootViewController(animated: true)
    return true
}

Here we read url.path, match it against known routes, and navigate to the corresponding screen.

Step 5: Test iOS Universal Links on a real device

Another notable distinguishing factor of Universal Links is that they are enforced by iOS at the system-level, which means testing in the simulator is unreliable.

So, always validate behavior on a physical device before shipping. You can start with basic validation:

  • Install the app on a real iPhone or iPad.
  • Trigger a Universal Link from Messages, Mail, or Notes.
  • Confirm the link opens the app instead of Safari.
  • Uninstall the app and tap the same link to confirm Safari fallback.

Then verify production scenarios:

What to testWhy it matters
Delete and reinstall the app after modifying the AASA fileiOS downloads and caches the AASA file at install time. Reinstalling forces revalidation.
Test different app states (cold start, backgrounded, already open)Universal Link routing behaves differently depending on lifecycle state.
Test from multiple surfaces (Messages, Mail, Notes, Safari address bar)Some system surfaces handle link activation differently.
Test a non-matching routeEnsures unknown paths fail safely instead of breaking navigation.

Troubleshooting Apple Universal Links

Remember what we were saying about iOS not showing errors clearly? This becomes doubly important when we’re troubleshooting.

Even when everything looks correctly configured, Apple Universal Links can fail silently. Links will simply open in Safari, or your app will start behaving weirdly.

So when troubleshooting we should start from observable behavior and trace the failure back to verification, domain configuration, AASA delivery, or routing logic inside the app.

Here are common symptoms and what they usually mean.

What you observeLikely cause and fix
The link always opens in Safari, even though the app is installedThe AASA file is missing, misconfigured, served with wrong headers, or the domain in Xcode does not match exactly. Verify hosting, headers, and applinks: entry.
The link worked before but stopped working after a server changeThe AASA file was modified but iOS cached the old version. Delete and reinstall the app to force revalidation.
The app opens, but nothing happensThe Universal Link handler is implemented, but routing logic does not map the path to a screen. Verify url.path handling.
Links work on one device but not anotherThe app may not have been reinstalled after configuration changes, or Associated Domains capability differs between builds.
example.com works but www.example.com does notThe subdomain was not declared separately in Xcode under Associated Domains. Add applinks:www.example.com.
The link briefly flashes Safari before opening the appA redirect (301/302) occurs before the final URL. Universal Links require direct responses with no redirect chain.
The link works in Messages but not when pasted in SafariSome surfaces trigger different behaviors. Ensure the link is tapped, not typed manually in the address bar.
Universal Links stopped working after enabling a CDNThe CDN may be caching or altering the AASA file headers. Confirm it returns HTTP 200 with application/json and no redirects.

Best practices for iOS Universal Links in production

Once Universal Links are working, our focus should shift to the long term: keeping domain verification and routing stable over time. Infrastructure changes and redirect chains are the most common sources of regression. The following tips and tricks will help with that.

  • Avoid redirect chains for marketing or tracking links. Universal Links require a direct response.
  • Keep the AASA file minimal and explicit. Remove unused paths and obsolete app IDs.
  • Monitor domain changes carefully. CDN, SSL, or hosting migrations can silently break verification.
  • Version and review routing logic inside the app to prevent stale or unhandled routes.
  • Maintain consistent behavior between app-installed and app-not-installed scenarios.

These practices reduce regressions and prevent Universal Links from breaking after infrastructure or product updates.

FAQs about iOS Universal Links

Why does my Universal Link open Safari instead of the app?

If the app is installed but the link opens Safari, iOS likely failed verification. This usually means the AASA file was not fetched correctly, the domain does not match exactly in Xcode, or the link was not tapped from a supported surface.


Do Universal Links work when typed directly into Safari?

No. Universal Links are designed to activate when a user taps a link. Manually entering the URL in Safari’s address bar does not reliably trigger the Universal Link behavior.


Why do Universal Links work on one device but not another?

iOS caches the AASA file when the app is installed. If configuration changes were made afterward, delete and reinstall the app to force revalidation. Also confirm both builds include the same Associated Domains entries.


Do Universal Links work inside apps like Instagram or Facebook?

Not always. Some third-party apps open links inside embedded webviews that do not trigger Universal Links. In those cases, the link may open the website instead of the app.


Can I use redirects or tracking links with Universal Links?

Redirect chains often break Universal Links. The final URL must respond directly over HTTPS without intermediate redirects for iOS to activate the app reliably.


Final thoughts on iOS Universal Links

iOS Universal Links become nice and predictable when domain verification, entitlements, and routing logic are aligned. But when something behaves differently on a real device, logs are often the only way to see what actually happened after the app opened.

Bugfender lets you capture real device logs remotely, making Universal Link debugging significantly easier at scale. Want to try it? You can create a free account on the Bugfender sign-up page.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Aleix Ventayol

Aleix Ventayol is CEO and co-founder of Bugfender, with 20 years' experience building apps and solutions for clients like AVG, Qustodio, Primavera Sound and Levi's. As a former CTO and full-stack developer, Aleix is passionate about building tools that solve the real problems of app development and help teams build better software.

Join thousands of developers
and start fixing bugs faster than ever.