All articles
Mobile DevelopmentJune 22, 2026 6 min read

Deep Links in React Native 2026: Universal Links, App Links, and the Expo Router Trap

Universal Links and Android App Links still break in production more than they should. Here's a field guide for getting them right in Expo Router apps without surprising the App Review team.

Deep Links in React Native 2026: Universal Links, App Links, and the Expo Router Trap

Deep links look trivial until a marketing team runs a campaign and 30% of the taps open Safari instead of your app. We've shipped enough React Native apps to know that deep linking is where platform politics, web infrastructure, and routing libraries collide — and Expo Router has changed the shape of the problem again in 2026.

This is the field guide we wish we'd had: how Universal Links and Android App Links actually verify, what Expo Router does on top, and the specific traps that keep biting teams during App Review and post-launch.

The Three Layers You're Actually Configuring

Deep linking in a React Native app is never one thing. It's at least three layers stacked on top of each other, and a failure at any layer looks identical from the user's perspective — the link opens in a browser tab.

  1. OS-level association: Apple's Universal Links (via apple-app-site-association) and Android App Links (via assetlinks.json). These tell the OS "this domain belongs to this app."
  2. App-level intent filters / associated domains: Declared in Info.plist and AndroidManifest.xml. Expo manages these through app.config.ts.
  3. In-app routing: Whatever takes the incoming URL and pushes the right screen. In Expo Router, this is mostly automatic — until it isn't.

If you only debug one layer, you'll chase ghosts for days. Always verify all three.

Why custom schemes (myapp://) aren't enough anymore

Custom schemes still work for inter-app handoffs and OAuth callbacks, but they can't be the primary deep link mechanism in 2026. iOS Mail strips them. Gmail rewrites them. Most messaging apps refuse to render them as tappable links. And both stores now scrutinise apps that rely on custom schemes for marketing flows.

Use https:// links backed by Universal Links / App Links for anything user-facing. Keep custom schemes for OAuth, payment returns, and dev builds.

Setting It Up in an Expo Router App

Expo Router (v3+) reads your file system and maps it to URLs. If you have app/product/[id].tsx, that becomes /product/:id. No Linking.createURL boilerplate, no manual linking config on the navigator. That part is genuinely nice.

Here's the minimum app.config.ts for a working setup:

export default {
  expo: {
    scheme: 'acme',
    ios: {
      bundleIdentifier: 'com.acme.app',
      associatedDomains: [
        'applinks:acme.com',
        'applinks:www.acme.com'
      ]
    },
    android: {
      package: 'com.acme.app',
      intentFilters: [
        {
          action: 'VIEW',
          autoVerify: true,
          data: [
            { scheme: 'https', host: 'acme.com' },
            { scheme: 'https', host: 'www.acme.com' }
          ],
          category: ['BROWSABLE', 'DEFAULT']
        }
      ]
    }
  }
};

The autoVerify: true is the bit teams forget. Without it, Android treats your links as "app links candidates" but won't open them in your app by default — users get a disambiguation dialog at best, the browser at worst.

The two files your web team has to host

On https://acme.com/.well-known/apple-app-site-association:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": ["TEAMID.com.acme.app"],
        "components": [
          { "/": "/product/*", "comment": "Product pages" },
          { "/": "/r/*", "comment": "Referral links" }
        ]
      }
    ]
  }
}

On https://acme.com/.well-known/assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.acme.app",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:..."
    ]
  }
}]

Both files must be:

  • Served over HTTPS with a valid certificate
  • Returned with Content-Type: application/json (Apple is strict, Google less so)
  • Not behind any redirect — Apple's CDN follows the first response only
  • Accessible without authentication, geo-blocking, or bot challenges (looking at you, Cloudflare)

The Failure Modes We Keep Finding

After enough audits, the same handful of problems show up across teams.

Wrong SHA-256 fingerprints on Android

This is the single most common Android deep link failure. You probably have at least three signing certificates in play: the local debug keystore, your upload key, and Google Play's app signing key. The fingerprint that matters in production is the Play app signing key, which you'll find under Play Console → Setup → App integrity.

If you ship assetlinks.json with only your upload key fingerprint, deep links work in internal testing tracks and silently break for everyone else. Include all relevant fingerprints in the array.

Cloudflare or WAF blocking Apple's crawler

Apple fetches apple-app-site-association from a CDN with a specific user agent. If your WAF rate-limits unknown bots or your Cloudflare config has "Bot Fight Mode" cranked up, the file might be reachable from your laptop but invisible to Apple. The link appears to work on a device you've already opened the app on (because iOS cached the association at install time) but fails on every fresh install.

Test with curl -A "AASA-Bot" https://acme.com/.well-known/apple-app-site-association from a server that isn't on your office IP.

Expo Router catch-alls swallowing query params

If you have an app/[...slug].tsx catch-all for marketing pages, it will happily match /product/123?ref=email and route to the wrong screen. Expo Router resolves routes by specificity, but a catch-all at the root level can still win over a deeper, more specific route if you've nested groups in a way that changes priority.

The fix: be explicit. Define /product/[id] before relying on catch-alls, and use the Expo Router devtools to inspect the resolved route tree.

Cold-start vs warm-start link handling

When the app is already running, Expo Router handles the URL transparently. When the app cold-starts from a link tap, there's a brief window where the initial URL is available via Linking.getInitialURL() but the router isn't mounted yet. If your splash screen logic dismisses before reading the initial URL, you land on the home screen instead of the deep-linked one.

With Expo Router, this is mostly handled — but only if you're using its built-in splash flow. Custom splash screens that manually call SplashScreen.hideAsync() need to wait for the router to be ready first.

App Review Pitfalls

Both stores now actively test deep links during review. A few things we've seen rejected:

  • Links that require auth without explaining why. If your reviewer taps a marketing link and gets a login wall with no context, expect a 2.1 rejection on iOS. Add a guest preview or a clear "sign in to view this product" screen.
  • Deep links to web-only content. If the link opens a WebView showing your marketing site, Apple will reject it as "not adding value beyond the web experience." Either route to native UI or don't claim the URL pattern.
  • Broken referral flows on Android. Play reviewers tap App Links from the listing description and expect them to open the app, not a Chrome Custom Tab. If autoVerify failed silently, you'll get a policy warning.

A Verification Checklist Before Release

We run through this on every release that touches routing:

  • AASA file returns 200, application/json, no redirects (curl -I)
  • assetlinks.json includes Play app signing SHA-256
  • Android verification status checked via adb shell pm get-app-links com.acme.app — every domain should show verified
  • iOS verification confirmed via Console.app filtering for swcd during a fresh install
  • Cold-start from email link lands on the correct screen
  • Warm-start while on another tab navigates correctly without remounting
  • Query params survive the round trip (test with ?utm_source=test)
  • Unhandled paths fall back gracefully, not to a white screen

Where We'd Start

If you're adding deep linking to an existing Expo app, don't try to boil the ocean. Pick one route — usually a product or content page — and get it end-to-end working on both platforms with Universal Links / App Links. Verify with the checklist above. Only then expand the AASA components array and add more route patterns.

If you're starting fresh, build the route file structure first, then layer the OS associations on top. Expo Router's filesystem-based routing makes the in-app side cheap; the expensive part is always the web infrastructure and the signing keys. Get those right early and the rest is mostly configuration.

If you'd rather hand the whole stack to a team that's done it before, our mobile engineering practice ships this regularly — and we've collected more war stories on the blog if you want to keep reading.

#React Native#Expo#Deep Linking#iOS#Android

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project