Expo scheme Field in app.json: Deep Linking Guide

Deep linking lets a URL open a specific screen inside your mobile app — powering push notification taps, marketing campaigns, password-reset flows, and social sharing. Without it, links either fail silently or drop users on a generic home screen.

Expo actually gives you three separate linking mechanisms, and mixing them up is the single biggest source of wasted debugging time: the scheme field in app.json for custom myapp:// URLs, iOS Universal Links (Apple’s Associated Domains plus an AASA file), and Android App Links (Google’s Digital Asset Links). This guide covers the exact app.json syntax for the scheme field — including its array form and character rules — the expo-linking API, and the terminal commands for all three, plus how to test each one before you ship.

Quick Answer

Add `”scheme”: “myapp”` inside the top-level `expo` object in app.json to make your app respond to `myapp://` URLs. `scheme` accepts either a single string or an array of strings (for multiple custom schemes), and each value must start with a lowercase letter followed only by lowercase letters, digits, `+`, `.`, or `-` — uppercase letters or underscores will fail validation.

For a true universal link (an `https://` URL that opens the app, or falls back to your website), add `associatedDomains` under `expo.ios` and `intentFilters` under `expo.android`, then host verification files at `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json`. Always run `npx expo prebuild –clean` and rebuild after any change — the scheme is compiled into the native binary.

What Is an Expo Universal Link?

A universal link in Expo is a standard HTTPS URL — for example `https://yourapp.com/products/42` — that opens directly inside your native app instead of a browser, on both iOS (Apple’s “Universal Links”) and Android (Google’s equivalent “App Links”). The key difference from a custom scheme like `myapp://` is that a universal link is a real, clickable web URL: it works in iMessage, email clients, Slack, and search results, and if the app isn’t installed it falls back gracefully to your website instead of a dead link or an error screen.

That fallback behavior is why universal links are the right choice for marketing emails, SMS campaigns, password-reset links, and anything that needs to keep working after an app uninstall. Custom schemes, by contrast, only work if the app is already present and are best reserved for internal navigation — push notification payloads or in-app share buttons where you control both ends of the link.

The scheme Field in app.json Explained

The `scheme` field is the entry point for all custom-URL deep linking in Expo. Add it directly inside the `expo` object: `{ “expo”: { “scheme”: “myapp” } }`. Once the app is built and installed, any URL in the format `myapp://` launches your app and passes the path and query string to your navigation layer.

`scheme` isn’t limited to a single string — the Expo config schema also accepts an array, so `”scheme”: [“myapp”, “myapp-dev”]` registers multiple custom schemes for one app, which is common when a debug build needs its own scheme so it can’t be confused with the production build. Every scheme value, whether top-level or platform-specific, must match the pattern `^[a-z][a-z0-9+.-]*$`: it has to start with a lowercase letter, and everything after that can only be lowercase letters, digits, `+`, `.`, or `-`. A scheme like `MyApp` or `my_app` will fail config validation at build time.

You can also set platform-specific values with `expo.ios.scheme` or `expo.android.scheme` (each accepting a string or array too). Per Expo’s own config reference, these are merged with the top-level `scheme` field rather than replacing it, so the app responds to both the shared scheme and any platform-specific scheme you add — useful for a white-labeled app that needs one extra scheme on a single platform.

If you omit the `scheme` field entirely, Expo Prebuild falls back to `ios.bundleIdentifier` and `android.package` as default schemes, so the app still accepts links like `com.yourcompany.myapp://` with no explicit configuration. One key constraint: `scheme` is compiled into the native binary at build time. It is not available in Expo Go, which uses the fixed `exp://` scheme for every project — you need a development build or a standalone build to test a custom scheme. After changing `scheme` you must run `npx expo prebuild –clean` and create a new build, either locally with `npx expo run:ios` / `npx expo run:android`, or in the cloud with `npx eas build`.

The expo-linking API: createURL, parse, and useLinkingURL

The `expo-linking` package gives you the JavaScript-side tools to build and read the URLs your `scheme` field enables. `Linking.createURL(path)` constructs a well-formed link using whatever scheme is configured in app.json — use it in code instead of hardcoding `myapp://` strings, so share buttons and QR codes stay correct if the scheme ever changes. Note that in Expo Go, the URL `createURL()` produces for published updates is neither stable nor predictable; for anything that needs a stable URL, like an OAuth callback, test with a development or standalone build.

`Linking.parse(url)` does the reverse: it takes an incoming URL and returns an object with the parsed `scheme`, `hostname`, `path`, and `queryParams`. For handling incoming URLs, `Linking.useLinkingURL()` is the current recommended hook — it returns the URL that launched the app on cold start and updates for any new URLs while the app is running, without briefly returning `null` on first render the way the older `useURL()` hook does. `useURL()` still works but is deprecated in favor of `useLinkingURL()`. `Linking.openURL(url)` lets your app open another app’s registered scheme (or a regular web URL) programmatically. If you’re using Expo Router, routing from these URLs is automatic — matching routes resolve directly from the parsed path without any manual `Linking.parse()` calls in most screens.

Step 1 — Add a Custom URL Scheme

Add the `scheme` field to app.json as shown above, then install `expo-linking` if it isn’t already a dependency (`npx expo install expo-linking`). Rebuild the native app with `npx expo prebuild –clean` followed by `npx expo run:ios` or `npx expo run:android` — a JS-only reload will not pick up the new scheme because it’s registered in the native `Info.plist` / `AndroidManifest.xml` at build time, not read at runtime.

Verify the scheme is registered by opening it from the command line: `npx uri-scheme open myapp://details/42 –ios` (or `–android`). If the app launches and your code receives the URL via `Linking.useLinkingURL()`, the scheme is wired correctly.

Step 2 — Configure iOS Universal Links

Add `associatedDomains` under `expo.ios` in app.json, one entry per domain, formatted as `applinks:yourapp.com`: `{ “expo”: { “ios”: { “associatedDomains”: [“applinks:yourapp.com”] } } }`. Then host an Apple App Site Association (AASA) file at `https://yourapp.com/.well-known/apple-app-site-association`, served with a `Content-Type` of `application/json` and no redirects — Apple’s servers fetch it directly and cache it, so it must be reachable without a login wall or extension.

Rebuild after adding `associatedDomains`, since it changes the native entitlements file. On a physical device (Universal Links do not work reliably in the iOS Simulator without extra setup), tap a link to your domain from Notes, Messages, or Safari to confirm it opens the app instead of the browser.

Step 3 — Configure Android App Links

Add `intentFilters` under `expo.android` in app.json, with `autoVerify: true` so Android performs Digital Asset Links verification automatically: `{ “expo”: { “android”: { “intentFilters”: [{ “action”: “VIEW”, “autoVerify”: true, “data”: [{ “scheme”: “https”, “host”: “yourapp.com”, “pathPrefix”: “/” }], “category”: [“BROWSABLE”, “DEFAULT”] }] } } }`.

Host the corresponding verification file at `https://yourapp.com/.well-known/assetlinks.json`, listing your app’s package name and the SHA-256 fingerprint of the signing certificate used for the build (debug and release keystores have different fingerprints, so add both while testing). Rebuild, install the APK/AAB, and check verification with `adb shell pm get-app-links com.yourcompany.myapp` — a `verified` status confirms Android will open links to your domain directly instead of prompting the user to choose an app.

Expo Router and the +native-intent File

If you’re on Expo Router, incoming deep links are matched to your file-based routes automatically — a link to `/products/42` resolves to `app/products/[id].tsx` without you writing a linking config by hand. When a third-party service sends links in a format that doesn’t match your route structure (shortened URLs, legacy paths, or an SDK you don’t control), create `app/+native-intent.tsx` and export a `redirectSystemPath` function to rewrite the incoming path before the router matches it.

`+native-intent` only runs on native platforms and is evaluated outside your normal component tree, so it’s the right place for auth redirects too — for example, sending an unauthenticated user to `/login?returnTo=` before the protected route ever renders. Returning `false` from `redirectSystemPath` disables Expo Router’s automatic handling entirely for that link, letting you take over routing yourself.

Testing Deep Links and Universal Links in Expo

For custom schemes, `npx uri-scheme open myapp://path –ios` or `–android` is the fastest test loop and works against a development build without touching a real server. For universal links, you need real hosted AASA/assetlinks files — Apple and Android both fetch these from your live domain, so `localhost` and ngrok tunnels without a stable HTTPS domain won’t reliably verify.

On iOS, use `xcrun simctl openurl booted https://yourapp.com/products/42` to simulate a tapped universal link in the Simulator, though full AASA verification is best confirmed on a physical device. On Android, `adb shell am start -a android.intent.action.VIEW -d “https://yourapp.com/products/42” com.yourcompany.myapp` opens the link directly, and `adb shell pm get-app-links` reports whether verification succeeded.

Common Mistakes

Forgetting to rebuild after changing `scheme`, `associatedDomains`, or `intentFilters` — none of these are read at runtime, so a Metro reload or OTA update will not apply them. Using an invalid scheme string (uppercase letters, underscores, or a leading digit) that passes locally in Expo Go but fails config validation on a real build.

Testing universal links only in Expo Go, where the fixed `exp://` scheme masks the fact that your custom scheme and associated domains were never actually wired up. Pointing `associatedDomains` or the AASA host at a staging domain with a login wall, which blocks Apple’s verification crawler and silently breaks the universal link in production.

expo app.json scheme field deep linking FAQs

What does the scheme field do in Expo’s app.json?

It registers one or more custom URL schemes (like myapp://) that launch your app when tapped. It accepts a single string or an array of strings, each matching the pattern of a lowercase letter followed by lowercase letters, digits, +, ., or -.

Can the scheme field hold more than one scheme?

Yes — pass an array, e.g. “scheme”: [“myapp”, “myapp-dev”], to register multiple custom schemes for the same app, commonly used to give a debug build a separate scheme from production.

Do ios.scheme and android.scheme replace the top-level scheme?

No. Per Expo’s config reference, schemes set under expo.ios.scheme or expo.android.scheme are merged with the top-level expo.scheme field, so the app responds to both.

What is an Expo universal link and how is it different from a custom scheme?

A universal link is a real https:// URL that opens your app on iOS/Android via Associated Domains and App Links, falling back to your website if the app isn’t installed. A custom scheme like myapp:// only works if the app is already present.

Do I need to rebuild my Expo app after changing the linking configuration?

Yes. scheme, associatedDomains, and intentFilters are compiled into the native binary, so you must run npx expo prebuild –clean and create a new build — a JS reload or OTA update won’t apply the change.

Why do my Android App Links show a disambiguation dialog instead of opening the app?

This usually means Digital Asset Links verification failed — check that assetlinks.json is hosted correctly, that autoVerify is true on the intent filter, and that the SHA-256 fingerprint matches the signing key of the installed build.

What happened to Firebase Dynamic Links?

Firebase Dynamic Links was deprecated by Google and shut down in 2025, which is why Expo’s own scheme, associatedDomains, and intentFilters configuration (rather than a third-party dynamic-link service) is now the standard approach for Expo apps.

Build It With GTStudios

Need help with your website, app, or small-business tech? GTStudios builds web, apps, and software for small businesses. See how GTStudios can help.