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 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.
- OS-level association: Apple's Universal Links (via
apple-app-site-association) and Android App Links (viaassetlinks.json). These tell the OS "this domain belongs to this app." - App-level intent filters / associated domains: Declared in
Info.plistandAndroidManifest.xml. Expo manages these throughapp.config.ts. - 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
autoVerifyfailed 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.jsonincludes Play app signing SHA-256 - Android verification status checked via
adb shell pm get-app-links com.acme.app— every domain should showverified - iOS verification confirmed via Console.app filtering for
swcdduring 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.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
App Size Bloat in React Native: Auditing Your Bundle Before Apple Notices
React Native apps quietly balloon past 100MB and tank install rates. Here's how we audit bundle size, hunt down the real culprits, and ship a leaner binary without rewriting in Swift.

Crash-Free Rate in React Native: Hitting 99.9% Without Going Native
How we pushed a React Native + Expo app from 99.2% to 99.9% crash-free sessions — what actually mattered, what was noise, and where Sentry, Hermes, and EAS fit in.

Background Tasks in React Native and Expo: What Actually Runs in 2026
Background execution on iOS and Android is a minefield of OEM quirks, OS budgets, and silent task kills. Here's what survives in production React Native and Expo apps in 2026 — and what to stop trying.
