expo universal links
react native deep linking
expo app links
ios universal links
android app links

Expo Universal Links: Your 2026 Implementation Guide

Expo Universal Links: Your 2026 Implementation Guide

You're probably dealing with one of two annoying outcomes right now. A link to your product, article, invite, or promo opens in Safari instead of your app, or it works on one platform and fails without indication on the other. Both feel small at first. In practice, they break onboarding flows, weaken campaigns, and create the kind of friction users rarely report but often abandon.

Expo Universal Links are one of those features that look straightforward in a checklist and messy in real implementation. The tricky part isn't writing a route handler. It's aligning your website, native entitlements, Android intent filters, hosting rules, and real-device testing so the operating system trusts your app enough to open it.

The good news is that the setup becomes manageable when you treat iOS Universal Links and Android App Links as one workflow instead of two unrelated tasks. That's the approach here. The web side establishes trust. The Expo config advertises what your app can handle. The build process bakes that into native code. Then your app reads the incoming URL and routes the user to the right screen.

Why Universal Links Are Worth the Effort

Broken deep linking creates a bad first impression fast. A user taps a shared product link expecting your app to open, lands in the browser instead, then has to log in again or find the content manually. That gap is where onboarding leaks and re-engagement weakens.

The business case for fixing it is stronger than many engineering guides admit. A Branch study found that enabling Universal Links can increase conversion-to-open rates by nearly 40% compared to apps without them, which makes this work more than a polish task. It directly affects whether users make it from a link tap into a live app session. Branch's published data and summary are available in Branch's Universal Links analysis.

What you're actually implementing

On iOS, Universal Links let standard HTTPS links open your installed app instead of Safari when the domain is verified and the route matches your app's entitlement.

On Android, App Links do the same kind of job through verified web intents. The naming differs, but the goal is identical. You want one public web URL that behaves intelligently across installed and non-installed states.

That matters in real products because users don't think in schemes or intent filters. They just tap links in Messages, Gmail, Notes, Slack, email campaigns, QR flows, and mobile web pages.

Practical rule: If marketing, product, and engineering all share the same canonical HTTPS URLs, your app linking strategy stays maintainable.

Why Expo developers get stuck

Most tutorials isolate one side of the problem. They either explain the native settings without the hosting requirements, or they show the association file without the Expo config that makes it work in a build.

Expo changes the implementation details, but not the operating system rules. That's why the right mental model is simple: your site proves it trusts the app, your app proves it's allowed to claim the site, and the OS verifies both before opening anything natively.

Understanding the Core Association Model

The system works like a two-way handshake. Your website publishes a file that names the app it trusts. Your app includes native configuration that declares which domains it's allowed to open. If those two sides line up, iOS and Android treat your HTTPS links as app-capable instead of browser-only.

A hand-drawn illustration showing website association files being uploaded to a server in the cloud.

The iOS trust relationship

Expo's documentation describes iOS Universal Links as a two-way association between the website and the app, using an apple-app-site-association file on the website and an Associated Domains entitlement in the app. Expo also notes that EAS Build helps register that entitlement automatically, which removes a common source of manual native errors. The official details are in Expo's iOS Universal Links documentation.

That detail matters because many failed setups come from half-configured trust. Developers upload the AASA file but forget the entitlement, or add the entitlement but never serve the AASA file correctly.

The Android equivalent

Android uses a similar trust pattern, but with different names. Your site hosts assetlinks.json, and your app declares intent filters that match the domain and path patterns you want Android to route into the app.

Android is usually a little more explicit in debugging because intent filters are visible in your app config and native manifest output. iOS is stricter and often feels more opaque because the verification and caching happen at the system level.

The OS isn't guessing. It's matching a web-hosted declaration against an app-bundled declaration, then deciding whether your link deserves native handling.

Why this model is useful

Once you understand the handshake, troubleshooting gets easier. You stop treating “opens in browser” as one bug and start narrowing it down:

  • Website side broken because the association file is missing, malformed, redirected, or served incorrectly
  • App side broken because Expo config didn't generate the expected native settings
  • Build side broken because you're testing in Expo Go or an outdated installed build
  • Route side broken because the app opens but doesn't parse or process the URL correctly

That's the core value of the association model. It gives you a clean way to isolate failure instead of changing random settings until something starts working.

Hosting Your Website Association Files

Most Expo Universal Links failures start on the server, not in React Native. The app config can be perfect and still do nothing if your domain doesn't serve the required files exactly where the OS expects them.

A mobile phone displaying Expo app configuration code for deep linking settings on a sketch background.

File placement that usually works

Host both files under /.well-known/ on your HTTPS domain:

  • iOS file at https://yourdomain.com/.well-known/apple-app-site-association
  • Android file at https://yourdomain.com/.well-known/assetlinks.json

Some teams also serve the iOS file from the site root for compatibility with older guidance, but if you're starting fresh, keep /.well-known/ as your primary target and make sure there's no redirect in the path.

Example AASA file for iOS

Use a minimal file first. Don't start with broad route complexity.

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

Replace TEAM_ID.com.example.app with your real Apple Team ID and bundle identifier combination.

Example assetlinks file for Android

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "YOUR_RELEASE_CERT_SHA256"
      ]
    }
  }
]

This file must match the Android package name and signing certificate fingerprint for the build you install.

Hosting rules that break setups most often

These files are small, but the serving rules are strict. Check these before you touch your app code again:

  • Use HTTPS: The verification expects a secure domain.
  • Avoid redirects: A redirect from /.well-known/... to another location often breaks verification.
  • Serve raw JSON or raw file content: Don't let a framework wrap the response in HTML.
  • Set a sensible content type: application/json is the safe default for most setups.
  • Keep auth off this path: No cookies, login walls, or bot protection challenges.

Example Express route

If you're serving from Node.js and Express, keep it blunt:

app.get('/.well-known/apple-app-site-association', (req, res) => {
  res.type('application/json');
  res.sendFile(path.join(__dirname, 'well-known', 'apple-app-site-association'));
});

app.get('/.well-known/assetlinks.json', (req, res) => {
  res.type('application/json');
  res.sendFile(path.join(__dirname, 'well-known', 'assetlinks.json'));
});

Example Nginx idea

In Nginx, the important part is direct access without rewrites. Serve the files from a real directory and bypass any app-router fallback that would otherwise return HTML.

If opening the file URL in a browser shows your site shell, a login page, or a framework 404 page, the OS won't trust it.

Keep the first version boring. Once verification works, expand path coverage. Teams often lose hours by trying to support every route pattern before they've proven the domain association itself is valid.

Configuring Your Expo Project for Linking

Expo Universal Links are established. Your server-side files establish trust. Your Expo config tells the generated native projects which domains and URL patterns the app can handle.

A hand-drawn illustration showing an Expo app project configuration on a laptop with a linking checklist.

Start with one shared URL strategy

Before you edit app.json or app.config.js, decide which public URLs belong to the app. Don't map everything.

A clean starting rule looks like this:

  • https://yourdomain.com/products/... opens product detail in the app
  • https://yourdomain.com/invite/... opens invite acceptance in the app
  • generic site pages such as /about stay in the browser

That boundary keeps your AASA components, Android intent filters, and in-app route handling aligned.

iOS config in Expo

For iOS, the key part is associatedDomains. In Expo config, it typically looks like this:

{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.example.app",
      "associatedDomains": ["applinks:yourdomain.com"]
    }
  }
}

If you support multiple subdomains, add each one explicitly. Don't assume www and apex are interchangeable unless you've configured both sides accordingly.

What this does is straightforward. During native build generation, iOS gets the entitlement that says your app may claim links for that domain. Without it, the AASA file on your site doesn't matter.

Android config in Expo

Android needs intent filters. In Expo config, the common structure looks like this:

{
  "expo": {
    "android": {
      "package": "com.example.app",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "yourdomain.com",
              "pathPrefix": "/products"
            },
            {
              "scheme": "https",
              "host": "yourdomain.com",
              "pathPrefix": "/invite"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

A few details matter here:

  • action: "VIEW" tells Android this is a link-opening intent.
  • BROWSABLE allows the app to receive intents from the browser and other apps.
  • DEFAULT makes the activity available for normal implicit intent resolution.
  • autoVerify: true tells Android to verify the host against your assetlinks.json.

If your links work as generic deep links but don't get verified App Link behavior, the issue is usually on the web verification side or signing side, not the route shape itself.

Managed workflow versus bare workflow

In the managed workflow, Expo generates the native projects for you from config. That's convenient, but it also means config drift can hide in old builds. If you change linking settings, rebuild and reinstall. Don't trust an existing app binary to pick up native changes.

In the bare workflow, treat Expo config as helpful but not magical. Verify the generated iOS entitlements and Android manifest match what you intended. Bare projects give you more control, but they also give you more places to be slightly wrong.

If you're still deciding how to structure an Expo app for production work, this guide on Expo app development services and delivery considerations is a useful companion for planning decisions around builds, native capabilities, and maintenance.

A practical app config example

Here's a compact app.config.js example that keeps both platforms in one place:

export default {
  expo: {
    name: 'ExampleApp',
    slug: 'example-app',
    scheme: 'exampleapp',
    ios: {
      bundleIdentifier: 'com.example.app',
      associatedDomains: ['applinks:yourdomain.com']
    },
    android: {
      package: 'com.example.app',
      intentFilters: [
        {
          action: 'VIEW',
          autoVerify: true,
          category: ['BROWSABLE', 'DEFAULT'],
          data: [
            {
              scheme: 'https',
              host: 'yourdomain.com',
              pathPrefix: '/products'
            },
            {
              scheme: 'https',
              host: 'yourdomain.com',
              pathPrefix: '/invite'
            }
          ]
        }
      ]
    }
  }
};

What works and what doesn't

What works is narrow matching and explicit ownership. Pick real routes, define them on both platforms, and keep the domain list short.

What usually doesn't work:

  • claiming too many domains early
  • changing config without producing a fresh native build
  • testing in Expo Go and expecting Universal Links or App Links to behave like a standalone app
  • mixing custom URL schemes and HTTPS routing rules without deciding which should own each use case

Use custom schemes for internal testing convenience if you want. Use verified HTTPS links for anything user-facing.

Building and Testing on Real Devices

This part isn't optional. Universal Links and App Links depend on OS-level verification, entitlements, installed app state, and domain trust. You won't get reliable answers from a simulator-only workflow.

The testing checklist that saves time

Follow this order and don't skip steps:

  1. Create a real build: Use EAS Build for a development build or production-style build that includes the native linking configuration.
  2. Install it on a physical device: Test on an actual iPhone and Android phone.
  3. Publish the association files first: Make sure your website files are already live before or by the time the app is installed.
  4. Trigger links from outside the app: Tap links from Notes, Messages, Mail, Gmail, Slack, or another external app.
  5. Reinstall after major native changes: If you change entitlements, package settings, or verification files, remove the app and install a fresh build.

What a valid test looks like

A good iOS test is tapping https://yourdomain.com/products/abc in Notes on an iPhone with the app installed. If verification succeeds and your route matches, the app opens directly to the matching screen or at least receives the URL.

A good Android test is similar, but Android may show app selection behavior differently depending on install state, verification state, and whether the link was previously associated with a browser preference.

Validation tools are worth using

Before blaming Expo, validate your hosted files independently:

  • Apple's App Search validation tooling helps confirm the iOS side is reachable and structured properly.
  • Google's Digital Asset Links checker helps confirm Android statement validity.

These tools won't solve every issue, but they quickly answer the question “is the domain association even valid?” That's often the fastest fork in the debugging path.

Common testing mistakes

  • Testing inside Expo Go: Expo Go doesn't represent your standalone native app configuration.
  • Copy-pasting a URL into Safari's address bar on iOS: That can behave differently from tapping a link in another app.
  • Changing files and expecting instant OS refresh: Device-side caching can make a fixed setup still look broken for a while.
  • Assuming install order never matters: In some cases, reinstalling after your web association file goes live clears stale state.

If the link opens your app only after a reinstall, that usually points to verification timing or cached association state, not random behavior.

Troubleshooting Common Universal Link Failures

When Expo Universal Links fail, the symptom is usually simple and the cause is not. A table is the fastest way to work through it.

Universal and App Link troubleshooting

Problem Symptom Likely Cause(s) How to Fix
Link opens in Safari on iPhone instead of the app AASA file isn't reachable, file content is malformed, associated domains entitlement is missing, or the installed build predates the config change Confirm the AASA file is publicly reachable at the expected path with no redirect. Rebuild with the correct ios.associatedDomains. Delete and reinstall the app. Test by tapping from Notes or Messages rather than typing into Safari
Link opens in browser on Android assetlinks.json doesn't match the package name or signing fingerprint, intentFilters don't match the path, or verification hasn't completed Check package name, signing fingerprint, host, and path prefixes. Rebuild and reinstall. Verify autoVerify is present and that the installed build is signed the way your asset links file expects
App opens, but lands on the wrong screen The app receives the URL, but your route parsing or navigation mapping is wrong Log the incoming URL in the app, parse it explicitly, and verify path-to-screen mapping. Keep your route patterns consistent between web URLs and in-app navigation
iOS shows an “Open in app” style handoff instead of opening directly User choice or previous OS behavior can affect how a domain is handled, especially after long-press actions or prior browser preference Re-test from a fresh context, avoid long-pressing and selecting browser actions, and reinstall if needed to reset local handling state
Works on iOS but not Android The website association file for Android is wrong, or Android intent filters are too narrow Compare your Android host and path matching against real URLs. Check signing details in assetlinks.json. Review a dedicated guide to Android Universal Links and App Links behavior if Android is the only failing side
Works on Android but not iOS The AASA file format or entitlement setup is wrong, or the file isn't served cleanly Re-check the raw AASA response, confirm no HTML or redirects, verify applinks:yourdomain.com is in the built entitlement, then reinstall
Build succeeds but links still don't work after config edits You changed native linking config without producing a fresh build Run a new EAS Build and install that binary. Native linking changes don't appear in JavaScript-only updates
EAS build or native generation complains about entitlements Config values are malformed or there's a mismatch between identifiers and native metadata Re-check your bundleIdentifier, associatedDomains, and Android package fields. Keep identifiers stable and consistent across config, store settings, and hosted association files

A useful debugging habit is to separate failures into three buckets. First, “domain verification failed.” Second, “native app wasn't built with the right config.” Third, “the app opened but routing failed.” Those are very different problems, and mixing them leads to wasted time.

Handling Incoming Links in Your App

Once the OS opens your app, your code still has work to do. You need to read the incoming URL, parse it, and route the user to the right screen.

Use expo-linking for this. It gives you a clean way to inspect the URL that launched or resumed the app.

import React, { useEffect } from 'react';
import * as Linking from 'expo-linking';

export default function LinkHandler() {
  const url = Linking.useURL();

  useEffect(() => {
    if (!url) return;

    const parsed = Linking.parse(url);

    if (parsed.path?.startsWith('products/')) {
      const slug = parsed.path.replace('products/', '');
      console.log('Navigate to product', slug);
    }

    if (parsed.path?.startsWith('invite/')) {
      const code = parsed.path.replace('invite/', '');
      console.log('Navigate to invite flow', code);
    }
  }, [url]);

  return null;
}

If you use React Navigation or Expo Router, connect the parsed path to your navigation logic instead of logging it. The key is consistency. The same route patterns you claimed in your association files and Expo config should map cleanly to screens in the app.

For a broader implementation pattern around parsing and routing URLs inside React Native apps, this guide to React Native deep linking patterns is a practical next read.


If your team wants help implementing Expo Universal Links, App Links, or a production-ready Expo architecture, Nerdify can help design, build, and validate the full flow across iOS, Android, and the web.