{"id":890,"date":"2026-04-23T17:00:00","date_gmt":"2026-04-23T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=890"},"modified":"2026-03-07T03:48:32","modified_gmt":"2026-03-07T08:48:32","slug":"expo-deep-linking","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/expo-deep-linking\/","title":{"rendered":"Expo Deep Linking: Setup Without Ejecting"},"content":{"rendered":"\n<p>Expo&#39;s managed workflow simplifies deep linking configuration by handling the native setup through <code>app.json<\/code> (or <code>app.config.js<\/code>). You don&#39;t need to touch Xcode or Android Studio directly. This guide covers deep linking setup in Expo, from basic scheme handling to Universal Links and App Links in production builds.<\/p>\n\n\n\n<p>For bare React Native setup, see <a href=\"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/\">React Native Deep Linking: Complete Setup Tutorial<\/a>. For navigation integration, see <a href=\"https:\/\/tolinku.com\/blog\/react-navigation-deep-links\/\">React Navigation Deep Links<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Understanding Expo&#39;s Linking Modes<\/h2>\n\n\n\n<p>Expo supports three types of deep links:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Expo Go Links (Development Only)<\/h3>\n\n\n\n<p>During development with Expo Go, deep links use the Expo scheme:<\/p>\n\n\n\n<pre><code>exp:\/\/192.168.1.5:8081\/--\/product\/123\n<\/code><\/pre>\n\n\n\n<p>These only work in development. You can&#39;t ship these to users.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Custom URL Schemes<\/h3>\n\n\n\n<p>Links like <code>yourapp:\/\/product\/123<\/code>. These work in development builds and production, but they&#39;re not as reliable as Universal Links\/App Links.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Universal Links (iOS) \/ App Links (Android)<\/h3>\n\n\n\n<p>HTTPS links like <code>https:\/\/go.yourapp.com\/product\/123<\/code>. These are the production standard and require domain configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Configure app.json<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Basic Scheme Configuration<\/h3>\n\n\n\n<p>Add a custom URL scheme to your <code>app.json<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;expo&quot;: {\n    &quot;scheme&quot;: &quot;yourapp&quot;,\n    &quot;ios&quot;: {\n      &quot;bundleIdentifier&quot;: &quot;com.yourapp&quot;,\n      &quot;associatedDomains&quot;: [&quot;applinks:go.yourapp.com&quot;]\n    },\n    &quot;android&quot;: {\n      &quot;package&quot;: &quot;com.yourapp&quot;,\n      &quot;intentFilters&quot;: [\n        {\n          &quot;action&quot;: &quot;VIEW&quot;,\n          &quot;autoVerify&quot;: true,\n          &quot;data&quot;: [\n            {\n              &quot;scheme&quot;: &quot;https&quot;,\n              &quot;host&quot;: &quot;go.yourapp.com&quot;\n            }\n          ],\n          &quot;category&quot;: [&quot;BROWSABLE&quot;, &quot;DEFAULT&quot;]\n        }\n      ]\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>scheme<\/code> field enables custom URL scheme handling (<code>yourapp:\/\/<\/code>). The <code>associatedDomains<\/code> and <code>intentFilters<\/code> fields enable Universal Links and App Links for your custom domain.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Using app.config.js for Dynamic Configuration<\/h3>\n\n\n\n<p>If you need environment-specific domains (staging vs production):<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ app.config.js\nconst IS_PROD = process.env.APP_ENV === &#39;production&#39;;\nconst LINK_DOMAIN = IS_PROD ? &#39;go.yourapp.com&#39; : &#39;staging-go.yourapp.com&#39;;\n\nexport default {\n  expo: {\n    scheme: &#39;yourapp&#39;,\n    ios: {\n      bundleIdentifier: IS_PROD ? &#39;com.yourapp&#39; : &#39;com.yourapp.staging&#39;,\n      associatedDomains: [`applinks:${LINK_DOMAIN}`],\n    },\n    android: {\n      package: IS_PROD ? &#39;com.yourapp&#39; : &#39;com.yourapp.staging&#39;,\n      intentFilters: [\n        {\n          action: &#39;VIEW&#39;,\n          autoVerify: true,\n          data: [{ scheme: &#39;https&#39;, host: LINK_DOMAIN }],\n          category: [&#39;BROWSABLE&#39;, &#39;DEFAULT&#39;],\n        },\n      ],\n    },\n  },\n};\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Handle Incoming Links<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Using expo-linking<\/h3>\n\n\n\n<p>Install the Expo linking module:<\/p>\n\n\n\n<pre><code class=\"language-bash\">npx expo install expo-linking\n<\/code><\/pre>\n\n\n\n<p>Handle deep links in your app:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import * as Linking from &#39;expo-linking&#39;;\nimport { useEffect } from &#39;react&#39;;\n\nexport function useDeepLinks(onLink) {\n  useEffect(() =&gt; {\n    \/\/ Handle links that opened the app (cold start)\n    Linking.getInitialURL().then((url) =&gt; {\n      if (url) {\n        onLink(parseLink(url));\n      }\n    });\n\n    \/\/ Handle links while app is running (warm start)\n    const subscription = Linking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n      onLink(parseLink(url));\n    });\n\n    return () =&gt; subscription.remove();\n  }, []);\n}\n\nfunction parseLink(url) {\n  const parsed = Linking.parse(url);\n  return {\n    path: parsed.path,\n    params: parsed.queryParams,\n  };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Using expo-router (Recommended)<\/h3>\n\n\n\n<p>If you&#39;re using Expo Router (file-based routing), deep linking is mostly automatic. Expo Router maps URL paths to file paths:<\/p>\n\n\n\n<pre><code>app\/\n  (tabs)\/\n    index.tsx       \u2192 \/\n    explore.tsx     \u2192 \/explore\n  product\/\n    [id].tsx        \u2192 \/product\/:id\n  invite\/\n    [code].tsx      \u2192 \/invite\/:code\n<\/code><\/pre>\n\n\n\n<p>The URL <code>https:\/\/go.yourapp.com\/product\/123<\/code> automatically navigates to <code>app\/product\/[id].tsx<\/code> with <code>id = &quot;123&quot;<\/code>.<\/p>\n\n\n\n<p>To access the parameter in the screen:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { useLocalSearchParams } from &#39;expo-router&#39;;\n\nexport default function ProductScreen() {\n  const { id } = useLocalSearchParams&lt;{ id: string }&gt;();\n\n  return &lt;Text&gt;Product: {id}&lt;\/Text&gt;;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">With React Navigation (Non-Router)<\/h3>\n\n\n\n<p>If you&#39;re using React Navigation instead of Expo Router:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import * as Linking from &#39;expo-linking&#39;;\n\nconst linking = {\n  prefixes: [\n    Linking.createURL(&#39;\/&#39;),           \/\/ Custom scheme: yourapp:\/\/\n    &#39;https:\/\/go.yourapp.com&#39;,        \/\/ Universal Links \/ App Links\n  ],\n  config: {\n    screens: {\n      Home: &#39;&#39;,\n      Product: &#39;product\/:id&#39;,\n      Invite: &#39;invite\/:code&#39;,\n      Promo: &#39;promo\/:slug&#39;,\n      NotFound: &#39;*&#39;,\n    },\n  },\n};\n\nfunction App() {\n  return (\n    &lt;NavigationContainer linking={linking}&gt;\n      &lt;Stack.Navigator&gt;\n        &lt;Stack.Screen name=&quot;Home&quot; component={HomeScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Product&quot; component={ProductScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Invite&quot; component={InviteScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;Promo&quot; component={PromoScreen} \/&gt;\n        &lt;Stack.Screen name=&quot;NotFound&quot; component={NotFoundScreen} \/&gt;\n      &lt;\/Stack.Navigator&gt;\n    &lt;\/NavigationContainer&gt;\n  );\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Development Testing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Testing with Expo Go<\/h3>\n\n\n\n<p>In Expo Go, you can test custom scheme links using the Expo URL format:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Open a link in Expo Go (development)\nnpx uri-scheme open &quot;exp:\/\/192.168.1.5:8081\/--\/product\/123&quot; --ios\n<\/code><\/pre>\n\n\n\n<p>Or use <code>Linking.createURL()<\/code> to generate the correct URL for the current environment:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const url = Linking.createURL(&#39;product\/123&#39;);\nconsole.log(url);\n\/\/ In Expo Go: exp:\/\/192.168.1.5:8081\/--\/product\/123\n\/\/ In production build: yourapp:\/\/product\/123\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Testing with Development Builds<\/h3>\n\n\n\n<p>For Universal Links testing, you need a development build (not Expo Go). Create one with:<\/p>\n\n\n\n<pre><code class=\"language-bash\">npx expo run:ios\n# or\nnpx expo run:android\n<\/code><\/pre>\n\n\n\n<p>Development builds include your native configuration (Associated Domains, intent filters), so Universal Links and App Links work as they would in production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing with EAS Build<\/h3>\n\n\n\n<p>For builds closer to production:<\/p>\n\n\n\n<pre><code class=\"language-bash\">eas build --platform ios --profile preview\neas build --platform android --profile preview\n<\/code><\/pre>\n\n\n\n<p>Install the preview build on a physical device and test links by tapping them from Messages, Notes, or email.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Domain Configuration<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">AASA File (iOS)<\/h3>\n\n\n\n<p>Your deep linking platform must serve the Apple App Site Association file at:<\/p>\n\n\n\n<pre><code>https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association\n<\/code><\/pre>\n\n\n\n<p>If using Tolinku, this is handled automatically. The file should include your Team ID and Bundle ID:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;apps&quot;: [],\n    &quot;details&quot;: [\n      {\n        &quot;appID&quot;: &quot;TEAMID.com.yourapp&quot;,\n        &quot;paths&quot;: [&quot;*&quot;]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">assetlinks.json (Android)<\/h3>\n\n\n\n<p>Served at:<\/p>\n\n\n\n<pre><code>https:\/\/go.yourapp.com\/.well-known\/assetlinks.json\n<\/code><\/pre>\n\n\n\n<p>Include your package name and signing certificate fingerprint. For EAS Build, you can find your fingerprint in the EAS dashboard or by running:<\/p>\n\n\n\n<pre><code class=\"language-bash\">eas credentials --platform android\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common Expo-Specific Issues<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Expo Go Doesn&#39;t Support Universal Links<\/h3>\n\n\n\n<p>Expo Go uses its own URL scheme (<code>exp:\/\/<\/code>). Universal Links and App Links only work in development builds or production builds. This is a common source of confusion: links work in testing (with the development build) but not in Expo Go.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Config Plugin Changes Require Rebuild<\/h3>\n\n\n\n<p>When you change <code>associatedDomains<\/code> or <code>intentFilters<\/code> in <code>app.json<\/code>, you need to rebuild the native project. These are native-level configurations that aren&#39;t applied by a JavaScript reload.<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Rebuild after config changes\nnpx expo prebuild --clean\nnpx expo run:ios\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Path Matching in Expo Router<\/h3>\n\n\n\n<p>Expo Router uses file-system routing. If your deep link path doesn&#39;t match a file in the <code>app\/<\/code> directory, it will show the not-found route. Make sure your file structure matches your link paths.<\/p>\n\n\n\n<p>For example, if your links use <code>\/product\/:id<\/code>, you need <code>app\/product\/[id].tsx<\/code> (not <code>app\/products\/[id].tsx<\/code>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debugging Link Resolution<\/h3>\n\n\n\n<p>Add logging to verify links are being received:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">import * as Linking from &#39;expo-linking&#39;;\n\nLinking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n  console.log(&#39;Received deep link:&#39;, url);\n});\n\nLinking.getInitialURL().then((url) =&gt; {\n  console.log(&#39;Initial URL:&#39;, url);\n});\n<\/code><\/pre>\n\n\n\n<p>Check the Metro bundler console or device logs for these messages when testing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Production Checklist<\/h2>\n\n\n\n<ul class=\"checklist wp-block-list\"><li><input type=\"checkbox\" disabled> <code>scheme<\/code> configured in app.json<\/li><li><input type=\"checkbox\" disabled> <code>associatedDomains<\/code> configured for iOS<\/li><li><input type=\"checkbox\" disabled> <code>intentFilters<\/code> configured for Android with <code>autoVerify: true<\/code><\/li><li><input type=\"checkbox\" disabled> AASA file accessible and valid<\/li><li><input type=\"checkbox\" disabled> assetlinks.json accessible with production signing fingerprint<\/li><li><input type=\"checkbox\" disabled> Deep link handling tested in development build (not Expo Go)<\/li><li><input type=\"checkbox\" disabled> Cold start deep links work on iOS and Android<\/li><li><input type=\"checkbox\" disabled> Warm start deep links work on iOS and Android<\/li><li><input type=\"checkbox\" disabled> All route paths match file structure (Expo Router) or linking config (React Navigation)<\/li><li><input type=\"checkbox\" disabled> Error\/not-found route handles unmatched paths<\/li><\/ul>\n\n\n\n<p>For integrating with the Tolinku SDK, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">React Native SDK documentation<\/a>. For cross-platform strategies, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">Cross-Platform Deep Linking Guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Add deep linking to Expo apps without ejecting. Configure app.json, handle linking in managed workflow, and test with Expo Go and builds.<\/p>\n","protected":false},"author":2,"featured_media":889,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Expo Deep Linking: Setup Without Ejecting","rank_math_description":"Add deep linking to Expo apps without ejecting. Configure app.json, handle linking in managed workflow, and test with Expo Go and builds.","rank_math_focus_keyword":"Expo deep linking","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-expo-deep-linking.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-expo-deep-linking.png","footnotes":""},"categories":[15],"tags":[25,23,20,181,24,71,69,56,22],"class_list":["post-890","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-app-links","tag-deep-linking","tag-expo","tag-ios","tag-javascript","tag-mobile-development","tag-react-native","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/890","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=890"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/890\/revisions"}],"predecessor-version":[{"id":891,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/890\/revisions\/891"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/889"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=890"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=890"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=890"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}