Expo RouterDeep LinkingUniversal LinksNavigation

Deep Linking in Expo Router: Universal Links and Android App Links Guide

Set up deep links in Expo Router — custom URL schemes, iOS Universal Links with apple-app-site-association, Android App Links with assetlinks.json, handling params, testing with uri-scheme and adb, and routing push notifications to screens.

DeveloperMill4 min read

Deep links turn your app into part of the web. A shared product link opens the product screen. A password reset email opens the reset form. A push notification lands on the exact chat. Expo Router makes this almost automatic, because every file is already a URL. Here's how to set up all three kinds of links and test them.

Every route is already a link

With Expo Router, your file structure is your link map:

app/
  index.tsx              → /
  products/[id].tsx      → /products/42
  settings/profile.tsx   → /settings/profile

Read params in the screen:

// app/products/[id].tsx
import { useLocalSearchParams } from "expo-router"

export default function Product() {
  const { id, ref } = useLocalSearchParams<{ id: string; ref?: string }>()
  // myapp://products/42?ref=email → id = "42", ref = "email"
}

New to file-based routing? Start with our Expo Router navigation guide.

1. Custom URL scheme (myapp://)

Add a scheme in app.json:

{
  "expo": {
    "scheme": "habitly"
  }
}

Rebuild your development build, and habitly://products/42 opens the product screen. Schemes are simple but limited: they only work when the app is installed, and they aren't verified, so any app can claim the same scheme. Use them for OAuth redirects, widgets, and internal links.

2. iOS Universal Links (https)

Universal Links open your app from regular https://yourdomain.com/... links, and fall back to your website when the app isn't installed.

In app.json (no https:// prefix):

{
  "expo": {
    "ios": {
      "associatedDomains": ["applinks:habitly.app"]
    }
  }
}

On your website, serve https://habitly.app/.well-known/apple-app-site-association (no file extension, valid JSON, HTTPS):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.yourcompany.habitly"],
        "components": [
          { "/": "/products/*" },
          { "/": "/invite/*" },
          { "/": "/admin/*", "exclude": true }
        ]
      }
    ]
  }
}

ABCDE12345 is your Apple Team ID. With Expo Router web, put the file in public/.well-known/ so it deploys with your site. Keep it under 128KB, and rebuild the app after changes so iOS re-fetches it.

3. Android App Links (https)

In app.json, add a verified intent filter:

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

On your website, serve https://habitly.app/.well-known/assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourcompany.habitly",
      "sha256_cert_fingerprints": ["AB:CD:EF:..."]
    }
  }
]

Get the fingerprint with:

eas credentials -p android

Copy the SHA256 Fingerprint for your build profile. If you distribute through Google Play with Play App Signing, also add the app signing key fingerprint from Play Console. A missing Play fingerprint is the most common reason App Links work in testing but not in production.

Testing deep links

Custom scheme:

npx uri-scheme open "habitly://products/42" --ios
npx uri-scheme open "habitly://products/42" --android

Android App Links:

adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://habitly.app/products/42" com.yourcompany.habitly

iOS Universal Links: paste the link into Notes or Messages on a real device and tap it. Typing it into Safari's address bar doesn't trigger Universal Links.

Deep links from push notifications

Send a url in the notification data and navigate when it's tapped:

import { useEffect } from "react"
import * as Notifications from "expo-notifications"
import { router } from "expo-router"

export function useNotificationDeepLinks() {
  useEffect(() => {
    const sub = Notifications.addNotificationResponseReceivedListener((response) => {
      const url = response.notification.request.content.data?.url
      if (typeof url === "string") router.push(url)
    })
    return () => sub.remove()
  }, [])
}

Send { "url": "/chats/123" } and the tap opens that chat. Full setup is in our push notifications guide.

Protect private routes

A deep link can point to a screen the user isn't allowed to see yet. Guard it at the layout level and redirect to sign-in. Expo Router's protected routes and our authentication guide show the pattern. After sign-in, send the user back to the original link.

Deep link checklist

  • scheme set, plus associatedDomains and intentFilters for https links
  • AASA and assetlinks.json served over HTTPS from /.well-known/
  • Play App Signing fingerprint included in assetlinks.json
  • Every linked screen handles missing or invalid params gracefully
  • Auth-gated screens redirect and return after sign-in
  • Tested on real devices with the production build

Deep links are also how widgets and Live Activities open the right screen. Our Expo templates give you an Expo Router structure that maps cleanly to shareable URLs.

Frequently asked questions

How does deep linking work in Expo Router?

Expo Router makes every file in your app/ directory a URL automatically. Once you set a `scheme` in app.json, a link like myapp://products/42 opens app/products/[id].tsx with id = 42 — no linking config needed. For https links, add Universal Links (iOS) and App Links (Android).

What's the difference between a URL scheme and a Universal Link?

A custom scheme (myapp://) only works if the app is installed and can be claimed by any app. Universal Links (iOS) and App Links (Android) use your real https domain, are verified by a file you host, open the app when it's installed, and fall back to your website when it's not.

Where do I host apple-app-site-association and assetlinks.json?

At https://yourdomain.com/.well-known/apple-app-site-association and https://yourdomain.com/.well-known/assetlinks.json, served over HTTPS. With Expo Router web, place them in public/.well-known/ so they're deployed with your site.

How do I test deep links in Expo?

Use `npx uri-scheme open myapp://products/42 --ios` (or `--android`) for custom schemes. For Android App Links, run `adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://yourdomain.com/products/42" <package>`. For iOS Universal Links, tap the link from Notes or Messages on a real device.

Build it faster with a template

Production-ready Expo React Native templates with the patterns from this post already wired up.

Browse templates