Guides
Expo Router + Appy: open the installed app, send everyone else to the right store
Point an Appy link at an Expo Router screen. Phones with your app open that screen, phones without it go to their store, and computers go to your website.
This guide is for Expo apps that use Expo Router and ship as a development or release build, made with EAS Build or npx expo run:ios and npx expo run:android. You give an Appy link a deep link to one of your screens. On Pro and Business no Appy code goes into the app: Appy’s page opens the deep link and sends phones without the app to their store. On Enterprise the React Native and Expo SDK adds Universal Links, App Links and deferred deep links.
What works on each plan
| Plan | What the link does | What you set up |
|---|---|---|
| Free | Sends iPhones and iPads to the App Store, Android phones to Google Play, AppGallery, Galaxy Store or GetApps, and computers to your website. It does not open the app. | Store URLs on the link. |
| Pro | Phones first try the link’s Deeplink URL: your scheme, such as acme://product/42, or an https link on your own domain that the app already handles. With a scheme, a phone without the app continues to its store. | scheme in the app config and a route for each screen you link to. |
| Business | The same, on your own domain such as go.acme.com. With parameter forwarding, query parameters on the link are added to the deep link, the store URLs and the website. | One DNS record. See Custom domains. |
| Enterprise | The link itself is a Universal Link and App Link on acme.appy.to or on your domain, so iOS and Android open the app without loading a page. After an install, the first launch receives the link, and you see which link brought each install. | react-native-appy and its config plugin. |
What happens when someone taps the link
A phone that opens a link with a Deeplink URL in a browser gets a short page from Appy instead of a redirect. iPhones, iPads, Android and Huawei phones all get it; computers go straight to the default destination URL.
| Where the link opens | App installed | App not installed |
|---|---|---|
| Safari, Chrome or another phone browser, with your scheme as the deep link | The page opens acme://product/42 and the app shows that screen. | The scheme opens nothing. After about five seconds the page continues to the store for that device; Continue now skips the wait. |
| A phone browser, with an https link on your domain as the deep link | On Android the page opens the link as an intent for your package and Android hands it to the app. On iPhone, iPad and Huawei phones the page shows an Open the app button; a tap opens the app. | On Android the intent continues to the store for that device. On iPhone, iPad and Huawei phones the page continues to the store after about five seconds; a tap on Open the app shows that page of your website instead. |
| The in-app browser of Instagram, Facebook, Messenger, TikTok, Snapchat, LinkedIn, X, Telegram, LINE or WeChat | Appy shows an Open the app page and waits for a tap. On Android the button is an intent with your package name, read from the Google Play URL. The page also explains how to open the link in Safari or Chrome. | The same page. Its second button opens the store for that device, or your website. |
The store is chosen per device, the same way as for links without a deep link: the App Store on iPhone and iPad, and Google Play, AppGallery, Galaxy Store, GetApps or a country rule on Android. A device without a store URL on the link goes to the default destination URL.
Register your scheme
{
"expo": {
"scheme": "acme",
"ios": { "bundleIdentifier": "com.acme.shop" },
"android": { "package": "com.acme.shop" }
}
}- A scheme starts with a lowercase letter and contains only lowercase letters, digits,
+,.and-. Pick one specific to your app rather than a common word. - The same keys work in
app.config.ts. - The scheme is part of the native project. After adding or changing it, make a new build; an older build and Expo Go don’t know it.
- Without
scheme, Expo registersios.bundleIdentifierandandroid.packageas schemes.
Map the deep link to a route
Every Expo Router screen has a URL. For a custom scheme, Expo Router reads everything after :// as the path, so the host is the first segment. For an https link it drops the domain.
| Deep link | Route | File |
|---|---|---|
acme://product/42 | /product/42 | src/app/product/[id].tsx |
acme://product/42?ref=spring | /product/42 with ref | src/app/product/[id].tsx |
acme://acme.com/product/42 | /acme.com/product/42 | No match, unless you rewrite it |
https://www.acme.com/product/42 | /product/42 | src/app/product/[id].tsx |
Projects from the default template keep routes in src/app since SDK 55; older projects use app. Read the route and query values with useLocalSearchParams:
import { useLocalSearchParams } from 'expo-router';
import { Text } from 'react-native';
export default function Product() {
const { id, ref } = useLocalSearchParams<{ id: string; ref?: string }>();
return <Text>Product {id}{ref ? ` from ${ref}` : ''}</Text>;
}Expo Router reserves screen, params, initial and state, so don’t use them as parameter names in your deep links.
Rewrite deep links that don’t match a file
If deep links you already use have another shape, rewrite them in src/app/+native-intent.tsx instead of changing every link. Expo Router passes each incoming URL to redirectSystemPath on iOS and Android before it picks a route:
export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
return path.replace('acme://acme.com/', 'acme://');
}Create the Appy link
- 1
Default destination
Create a link in the dashboard. The Default destination URL is where computers go, usually the matching page of your website.
- 2
Store URLs
Under Mobile app store overrides, paste the full App Store URL and the Google Play URL in the
https://play.google.com/store/apps/details?id=com.acme.shopform. Appy reads your package name fromidfor in-app browsers on Android. - 3
Deep link
Under Deeplink, turn on Enable deeplink redirect and enter
acme://product/42as Deeplink URL.
On Business you can create the same link with the REST API:
{
"slug": "acme-spring",
"fallbackUrl": "https://www.acme.com/spring",
"deeplinkUrl": "acme://product/42",
"targets": {
"iphone": "https://apps.apple.com/app/id123456789",
"android": "https://play.google.com/store/apps/details?id=com.acme.shop"
}
}The Deep Link Generator builds the scheme link and the Android intent URL from your scheme and path, if you want to check them before you create the link.
An https link on your own domain
If the app already handles Universal Links and App Links on your domain, the Deeplink URL can be one of them, such as https://www.acme.com/product/42. The domain goes into the app config:
{
"expo": {
"ios": {
"associatedDomains": ["applinks:www.acme.com"]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [{ "scheme": "https", "host": "www.acme.com", "pathPrefix": "/product" }],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}- Write
associatedDomainsentries withouthttps://. - Your domain serves
/.well-known/apple-app-site-associationand/.well-known/assetlinks.jsonover HTTPS. The AASA and assetlinks generator writes both, and the Universal Link Validator checks them. - The Android fingerprint is the SHA256 Fingerprint that
eas credentials -p androidlists for your build profile, or Google Play’s app signing key in Play Console if you don’t sign with EAS. - iOS downloads the association file when the app is installed or updated, so a change reaches existing users with the next App Store update.
People without the app still reach the store
On Android, Appy opens an https deep link as an intent for your package, read from the Google Play URL, and an intent that finds no app continues to the store for that device. Safari doesn’t open Universal Links from a script, so on iPhone, iPad and Huawei phones Appy’s page shows an Open the app button and continues to the store after about five seconds. Only a tap on that button without the app installed shows your website. Analytics count those clicks as website visits, not as opened apps.
Your own domain and parameters (Business)
- Add
go.acme.comas a custom domain. Every link then also works ashttps://go.acme.com/acme-spring, with the same deep link behaviour. - Turn on parameter forwarding for the link. A tap on
https://go.acme.com/acme-spring?ref=newsletterthen opensacme://product/42?ref=newsletter, and the same parameter goes to the store and website URLs. A key the deep link already sets keeps its value. - On Business, the custom domain carries your links. Universal Links and App Links on it, which open the app without a page, are part of Enterprise.
Universal Links, App Links and deferred deep links (Enterprise)
On Enterprise, Appy serves the association files for your app on its subdomain, such as acme.appy.to, or on your custom domain, and react-native-appy receives the links. The full setup is in the React Native and Expo SDK guide. Compared with Pro:
- The link opens the app directly: iOS and Android hand
https://acme.appy.to/springto the app before any page loads. Without the app, the same link goes to the store. - The first launch after an install receives the link the person tapped, so a new user lands on the same screen, and you see which link brought each install.
- Keep
schemein the app config and the scheme deep link on your links. In-app browsers that ignore Universal Links open the app through Appy’s page with it.
{
"expo": {
"scheme": "acme",
"plugins": [
["react-native-appy", { "linkDomains": ["acme.appy.to"] }]
]
}
}Expo Router would also try to open /spring for https://acme.appy.to/spring. Return null for Appy links in +native-intent.tsx and route them from Appy.addDeepLinkListener, as shown in Expo Router and React Navigation. For links on go.acme.com, see App links on your domain.
Test on a simulator and a device
Run a development or release build that includes your scheme; Expo Go uses its own exp:// scheme. Test the deep link on its own first:
npx uri-scheme open "acme://product/42" --ios
npx uri-scheme open "acme://product/42" --android
xcrun simctl openurl booted "acme://product/42"
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "acme://product/42"Then open the Appy link, which goes through Appy’s page like a real tap:
xcrun simctl openurl booted "https://appy.to/acme-spring"
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://appy.to/acme-spring"- App installed: the app opens on the product screen.
- App removed: after about five seconds the page continues to the App Store or Google Play.
- On a phone, send yourself the link in Notes or Messages and tap it, as your users would. Typing a URL into the address bar doesn’t always open an app.
- Inside Instagram or TikTok, Appy shows the Open the app page. Tap Open the app.
- On a computer, the link opens the default destination URL.
- With an https deep link on Android,
adb shell pm get-app-links com.acme.shoplists your domain asverifiedonce Android has checked it.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The store opens although the app is installed | The build doesn’t register the scheme: it was made before you added scheme, it is Expo Go, or the Deeplink URL uses another scheme. | Make a new build after changing scheme and check that the Deeplink URL starts with exactly that scheme. npx uri-scheme open with the same URL must open the app. |
| The app opens, but on a not-found screen | The path matches no file. With a custom scheme the host is the first segment, so acme://acme.com/product/42 becomes /acme.com/product/42. | Use acme://product/42, or rewrite the URL in +native-intent.tsx. |
| Nothing happens inside Instagram, TikTok or Facebook | In-app browsers don’t always let a page open another app. | The person taps Open the app on Appy’s page, or opens the page in Safari or Chrome as it explains. Check that the Google Play URL contains ?id=. |
| An https deep link shows your website instead of the app | The association file or the app’s domain setup is wrong, or the person tapped Open the app without the app installed. | Check the domain with the Universal Link Validator and adb shell pm get-app-links. Or use the scheme as the Deeplink URL. |
| A query parameter is missing on the screen | Parameter forwarding is off, the deep link already sets that key, or the name is reserved by Expo Router. | Turn on parameter forwarding (Business), rename screen, params, initial or state, and read values with useLocalSearchParams. |