Reference for deep links, universal links and app links in React Native apps. Use when links do not open the app, when setting up apple-app-site-association or assetlinks.json, when cold-start links get lost, or when configuring expo-router linking and custom URL schemes.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add AnilBurcu/claude-code-react-native --skill deep-linking --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Deep Linking?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anilburcu-deep-linking)More formats (shields.io, HTML) on the badges page.
---
name: deep-linking
description: Reference for deep links, universal links and app links in React Native apps. Use when links do not open the app, when setting up apple-app-site-association or assetlinks.json, when cold-start links get lost, or when configuring expo-router linking and custom URL schemes.
user-invocable: false
---
# Deep links that open the app every time
## Two layers, different jobs
- **Custom schemes** (`myapp://...`): trivial to set up, work from inside your own emails and QR flows, but any app can claim a scheme and browsers treat them as second-class. Fallback layer.
- **Universal links (iOS) / App links (Android)** (`https://yourdomain.com/...`): the real thing. They need server-side proof files and that is where every setup fails.
Ship both: schemes for internal redirects (OAuth return, payment return), https links for anything a user might tap outside the app.
## iOS: the AASA file and Apple's CDN
The proof file lives at `https://yourdomain.com/.well-known/apple-app-site-association`: HTTPS, no redirects, JSON body, appID as `TEAMID.bundleid`. The entitlement side is `applinks:yourdomain.com` (Expo: `associatedDomains` in app config).
The part that costs people days: **devices do not fetch your file directly**. Apple's CDN fetches it (first pickup within about a day) and caches it, so an AASA fix is not live when your server says it is. For development there is an alternate mode: entitlement `applinks:yourdomain.com?mode=developer` plus the device's developer mode skips the CDN. Use it for iteration, remove it for release builds.
If links still open Safari instead of the app: long-press the link and check for the "Open in app" option, confirm the file is reachable with `curl -i` (status 200, no redirect chain), and remember that pasting a URL into Safari's address bar deliberately does NOT trigger universal links; tap one from Notes or Messages instead.
## Android: assetlinks.json and the signing key trap
The proof file lives at `https://yourdomain.com/.well-known/assetlinks.json` with the app's package name and the SHA-256 fingerprint of the **certificate that signs what ships**. The trap: with Play App Signing (the norm), Google re-signs your app, so the fingerprint must come from Play Console's App integrity page (the app signing key), NOT from your upload key or local keystore. A local-keystore fingerprint makes verification fail silently and only in production, which is the worst combination.
Add fingerprints additively: dev builds and store builds can both live in the file. Intent filters need `android:autoVerify="true"` on the https filter. Check verification state on a device with:
```bash
adb shell pm get-app-links your.package.name
```
`verified` next to the domain is the goal; anything else names the failing domain.
## Cold start vs warm start, the classic lost link
A link that launches the app arrives through the initial-URL path; a link tapped while the app runs arrives through the URL event. Handling only the event listener means cold-start links vanish, and that is the most common deep-link bug report. expo-router handles both when the scheme and domains are configured, but anything custom (auth guards, splash gates, onboarding redirects) must re-apply the pending link AFTER the gate resolves, or the user lands on the home screen and the link dies.
Test both paths explicitly: once with the app killed, once with it backgrounded.
## Testing without a website deploy
```bash
# custom scheme, both platforms
npx uri-scheme open "myapp://lesson/42" --ios
npx uri-scheme open "myapp://lesson/42" --android
# https links on Android without waiting for verification
adb shell am start -a android.intent.action.VIEW -d "https://yourdomain.com/lesson/42" your.package.name
```
iOS https links cannot be faked from the terminal in the same way; that is what `mode=developer` exists for.
## Adjacent traps
- OAuth and payment providers redirect back via your scheme or https link; a changed scheme breaks sign-in in the field, so treat the scheme as API surface and never rename it casually.
- Deferred deep links (install, then first open lands on content) are not a platform feature; they require an attribution/linking service. Do not promise the product team otherwise.
- Every route a link can open must handle "referenced thing is gone" gracefully; links outlive content.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!