Deep Linking Best Practices
A condensed summary of 25 deep linking essentials for Expo SDK 57 apps - idempotent routes, safe fallbacks for bad URLs, and verification hygiene drawn from every page in this section.
Search across all documentation pages
A condensed summary of 25 deep linking essentials for Expo SDK 57 apps - idempotent routes, safe fallbacks for bad URLs, and verification hygiene drawn from every page in this section.
Declare scheme in app.config.ts before shipping any deep link: Custom schemes are the fastest dev path and the OAuth redirect backbone - without a registered scheme, createURL and store binaries cannot claim inbound URLs.
Prefer verified HTTPS links for consumer marketing: Universal Links and App Links open without disambiguation sheets - custom schemes are fine for dev and OAuth but frustrate email campaigns when another app registers the same scheme.
Rebuild native binaries after scheme or domain changes: Entitlements and intent filters are baked at prebuild - OTA updates cannot add associatedDomains or autoVerify intent filters.
Use Linking.createURL for outbound links - never hardcode myapp://: Flavors, prefixes, and triple-slash Android quirks are handled centrally - hardcoded strings break across staging and production.
Let Expo Router map file paths to URLs when using file-based routing: app/orders/[id].tsx already handles /orders/:id - duplicate manual parsers drift from route files and typed routes.
Centralize inbound URL handling in one root hook: Cold start (getInitialURL), warm start (addEventListener), push taps, and deferred first-open should converge on one validated navigation function - scatter causes double pushes.
Gate deep-link navigation on authReady and onboarding completion: Protected screens that mount before session restore flash data and drop return URLs - queue intent until bootstrap finishes.
Validate route params with Zod at the linking boundary: Deep links are untrusted input - malformed id values should router.replace("/") or a dedicated error screen, not throw during render.
Treat query params as public - never put secrets in URLs: token, email, and code query keys land in analytics, logs, and referrer headers - exchange one-time codes server-side after navigation.
Handle cold start and warm start explicitly: getInitialURL alone misses background taps; listener-only setups miss kill-state launches - implement both with cleanup and duplicate guards.
Use router.replace for magic links and password resets: Users should not back-navigate into pre-auth stacks - transactional one-shots replace; browsable content pushes.
Make routes idempotent: Tapping the same orders/42 link twice should not corrupt stack depth - router.push to the same screen may duplicate; consider router.navigate or dedupe by path ref.
Host apple-app-site-association with no redirects and application/json: iOS rejects redirected or HTML-wrapped AASA - serve 200 on apex and /.well-known/ paths with valid TLS.
Host assetlinks.json with Play App Signing SHA-256 fingerprints: Upload-key fingerprints alone fail verification on Play-distributed builds - copy the cert from Play Console → App integrity.
Narrow pathPrefix and AASA paths - do not claim /: Over-broad paths hijack blog and admin URLs into the app - use explicit prefixes and NOT exclusions.
Run adb shell pm get-app-links on Android QA builds: Verification status is deterministic - legacy_failure before device tap saves hours of guessing.
Test universal links on TestFlight and internal tracks - not only simulators: iOS caches AASA on device; simulator pass + device Safari fallback is a common false positive.
Kill the app between cold-start test cases: Warm-start-only QA misses the highest crash segment - Maestro stopApp: true and manual kill-before-tap are mandatory.
Put routing data in push data payload - not only title/body: Notification tap handlers read content.data - parse discriminated types and validate before router.push.
Pair push cold start with getLastNotificationResponseAsync: Same pattern as getInitialURL - listener-only push handling fails when the app was killed.
Plan deferred deep links without assuming iOS install referrer: Android Install Referrer plus server-side click IDs are the privacy-durable pattern - MMP SDKs require ATT and privacy disclosures when tracking across apps.
Offer a web fallback and manual continue CTA when deferred match fails: 30–50% of deferred attribution misses - land on a sensible default with "Continue your offer?" rather than a blank home screen.
CI-verify hosted AASA and assetlinks.json on every web deploy: JSON hosting breaks silently when marketing changes CDN rules - script fetch + JSON.parse before native release.
Contract-test URL parsing in Jest - Maestro cannot cover every malformed input: Unit tests for Linking.parse → route mapping are cheap and block %20, empty ids, and injection attempts.
Document schemes, domains, and test matrix in the release runbook: Deep links touch app.config.ts, CDN, push payloads, and auth - tie P0 matrix rows to release checklist owners so linking is not an afterthought on submission day.
expo-linking for createURL, openURL, and custom pre-navigation.scheme config.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026