Key takeaways
- You do not need to eject to get full native deep linking in Expo. Expo's config plugins and prebuild workflow give you Universal Links and App Links with native access, no ejecting required.
- Expo Router handles basic in-app deep linking automatically based on your file structure, but Universal Links and App Links still need the same AASA and assetlinks.json files as any native app.
- Deferred deep linking, routing a brand-new install to the right screen, is not something Expo or React Navigation solve on their own. That still needs a dedicated service or SDK layered on top.
Why "ejecting" is the wrong frame in 2026
The expo eject command that used to force a one-way move into a bare React Native project has been gone for years. In its place, Expo uses config plugins and a prebuild step that generate native iOS and Android projects from your Expo config, giving you full native module access, including the entitlements and manifest changes deep linking requires, without giving up Expo's managed workflow or its build tooling.
That means the real question for deep linking in Expo is not "do I need to eject," it is "have I run a development build," since custom native configuration, including a deep linking scheme, is baked into the native binary and is not available inside the standard Expo Go app.
Step 1: add a custom scheme
The simplest form of deep linking is a custom URL scheme, declared in your app config.
{
"expo": {
"scheme": "myapp",
"ios": { "bundleIdentifier": "com.yourcompany.myapp" },
"android": { "package": "com.yourcompany.myapp" }
}
}
This makes myapp:// links open your app once it is installed. If you do not set a scheme explicitly, Expo Prebuild falls back to using your bundle identifier and package name as the default scheme for both platforms.
After adding or changing the scheme, you need a new development build, since the scheme is compiled into the native binary rather than read at runtime.
npx expo prebuild --clean
npx expo run:ios
npx expo run:android
Step 2: handle the link with expo-linking
The expo-linking module gives you the utilities to read and parse incoming URLs regardless of whether you are running in a development build or Expo Go, where the scheme differs.
import * as Linking from 'expo-linking';
const url = Linking.useLinkingURL();
if (url) {
const { hostname, path, queryParams } = Linking.parse(url);
// route based on path and queryParams
}
If you are using Expo Router, this is largely automatic: routes are inferred from your file structure, and a link to /product/123 opens the matching route file without you writing manual parsing logic.
Step 3: add Universal Links and App Links
Custom schemes are the right starting point because they need the least configuration and let you confirm your routing logic before adding the complexity of domain verification, but production apps should move to Universal Links (iOS) and App Links (Android). These use standard https:// URLs, work whether or not the app is installed, and cannot be intercepted by another app the way an unverified scheme can be.
For iOS, add your associated domains to the Expo config:
{
"expo": {
"ios": {
"associatedDomains": ["applinks:yourdomain.com"]
}
}
}
You still need to host an apple-app-site-association file at /.well-known/apple-app-site-association on that domain, exactly as you would in a bare native app. Expo does not generate or host that file for you.
For Android, App Links verification works the same way it does in any native Android app: an assetlinks.json file hosted at /.well-known/assetlinks.json on your domain, matched against an intent filter with autoVerify set, which Expo's config plugins generate into the native manifest from your app config.
A new development build is required after adding either configuration, since both are compiled into the native project during prebuild.
Step 4: deferred deep linking
None of the above solves the install gap. A user who does not have your app yet gets sent to the App Store or Play Store, and neither Expo, React Navigation, nor the underlying OS passes the original link data through to the freshly installed app. This is true regardless of managed workflow, prebuild, or bare project, since it is a platform-level gap, not an Expo limitation.
Solving it requires the same matching approach any native app needs: a service that records the click, then matches the app's first open back to it using a deterministic method like Android's Play Install Referrer API where available, or fingerprint matching as a fallback.
Integrating a deep linking SDK in Expo
Because Expo apps compile down to real native iOS and Android binaries through prebuild, a deep linking SDK integrates the same way it would in a bare project. Ulinkly ships an Expo config plugin, so you add it to expo.plugins and prebuild wires up the associated domains and intent filters for you. Install it with npx expo install @ulinkly/react-native, add the plugin, then initialize and subscribe:
{
"expo": {
"plugins": [
["@ulinkly/react-native", { "domains": ["yourdomain.com"] }]
]
}
}
import ULink, { ULinkResolvedData } from '@ulinkly/react-native';
// Initialize once at startup, and always await it
await ULink.initialize({ apiKey: 'YOUR_API_KEY' });
// Subscribe at a stable app root, inside a useEffect
const dynSub = ULink.onDynamicLink((data: ULinkResolvedData) => {
if (data.slug) router.push(`/${data.slug}`); // route on the resolved path/params
});
const uniSub = ULink.onUnifiedLink((data: ULinkResolvedData) => {
if (data.slug) router.push(`/${data.slug}`);
});
// Clean up with dynSub.remove() / uniSub.remove(); never call ULink.dispose() on unmount.
@ulinkly/react-native is a native module, so it needs a development build (npx expo run:ios / run:android) or prebuild, not Expo Go. Full details are in the React Native SDK guide.
Frequently asked questions
Can I do deep linking in Expo without ejecting?
Yes. The eject command was removed years ago. Expo's config plugins and prebuild workflow generate the native iOS and Android configuration deep linking requires, including entitlements and manifest changes, while keeping you inside Expo's managed tooling.
Does Expo Router handle deep linking automatically?
Expo Router handles routing for links you have already gotten into the app, matching a URL path to the corresponding route file automatically. It does not handle the AASA or assetlinks.json hosting and verification that Universal Links and App Links require, which you still configure separately.
Why doesn't my custom scheme work in Expo Go?
The scheme specified in your Expo config only applies to standalone and development builds. Inside Expo Go, deep links use a different exp:// format tied to your local dev server address, so testing your production scheme requires a development build, not Expo Go.
Does Expo support deferred deep linking out of the box?
No. Deferred deep linking, routing a new install to the intended screen, requires a matching service that reconnects the click to the install after the app store detour. This applies to Expo apps the same way it applies to any native iOS or Android app, since the gap is a platform limitation, not something Expo's tooling addresses.
Want deferred deep linking that works out of the box in Expo?
Ulinkly ships an Expo config plugin and native SDKs that work with Expo's prebuild workflow. Start free for up to 10,000 monthly active users at ulink.ly/pricing.
Sources
- Expo: Deep linking overview: https://docs.expo.dev/linking/into-your-app/
- Expo: iOS Universal Links: https://docs.expo.dev/linking/ios-universal-links/
- Expo Router documentation: https://docs.expo.dev/router/introduction/
