{"id":1283,"date":"2026-06-01T09:00:00","date_gmt":"2026-06-01T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1283"},"modified":"2026-03-07T03:49:04","modified_gmt":"2026-03-07T08:49:04","slug":"android-app-links-pwa","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/android-app-links-pwa\/","title":{"rendered":"App Links for Progressive Web Apps on Android"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Progressive Web Apps (PWAs) and native Android apps are not mutually exclusive. Many products ship both: a PWA for broad reach and a native app for deeper platform integration. The challenge is making links work seamlessly between them. When a user taps a link, should they land in the PWA or the native app? What if they have both installed? What if the native app uses a Trusted Web Activity (TWA) to wrap the PWA?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how App Links interact with PWAs and TWAs on Android, and how to configure both experiences to coexist. For the App Links foundation, see the <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">Android App Links complete guide<\/a>. For web-to-app bridging strategies, see <a href=\"https:\/\/tolinku.com\/blog\/deep-linking-for-web\/\">deep linking for web<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">PWA vs. Native App: The Link Conflict<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you have both a PWA and a native app for the same domain, a link conflict arises:<\/p>\n\n\n\n<pre><code>User taps https:\/\/yourapp.com\/products\/123\n  \u2192 Should the PWA open? (installed via &quot;Add to Home Screen&quot;)\n  \u2192 Should the native app open? (installed via Play Store)\n  \u2192 Should Chrome open? (if neither is the default)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Android resolves this through intent priority:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Verified App Links<\/strong> (native app) take highest priority. If the native app has verified App Links for the domain, it opens.<\/li>\n<li><strong>TWA apps<\/strong> are treated similarly to native apps for link handling.<\/li>\n<li><strong>Installed PWAs<\/strong> (added to home screen) are lower priority than verified native apps.<\/li>\n<li><strong>Browser<\/strong> is the fallback.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This means: if a user has both your PWA and native app installed, and the native app has verified App Links, the native app always wins. The PWA never gets the link.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Trusted Web Activities (TWA)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A <a href=\"https:\/\/developer.chrome.com\/docs\/android\/trusted-web-activity\/\" rel=\"nofollow noopener\" target=\"_blank\">Trusted Web Activity<\/a> wraps your PWA in a native Android shell. It uses Chrome&#39;s rendering engine but runs full-screen without browser UI. From the Play Store&#39;s perspective, it&#39;s a native app. From the user&#39;s perspective, it looks like a native app. From the deep linking system&#39;s perspective, it behaves like a native app.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">TWA and App Links<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">TWAs use the same Digital Asset Links verification as regular App Links. The TWA&#39;s domain must have an <code>assetlinks.json<\/code> file that includes the TWA&#39;s package name and signing key.<\/p>\n\n\n\n<pre><code class=\"language-json\">[{\n  &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n  &quot;target&quot;: {\n    &quot;namespace&quot;: &quot;android_app&quot;,\n    &quot;package_name&quot;: &quot;com.example.twa&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;YOUR_SHA256_FINGERPRINT&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When a link to your domain is tapped, the TWA opens full-screen (no browser bar) if verification passes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">TWA Deep Link Handling<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">TWAs handle deep links by loading the corresponding URL in the web activity. The path from the deep link maps directly to a URL in your PWA:<\/p>\n\n\n\n<pre><code>Deep link: https:\/\/yourapp.com\/products\/123\nTWA opens: https:\/\/yourapp.com\/products\/123 (full-screen, no browser UI)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your PWA&#39;s client-side routing handles the rest. If your PWA uses a framework router (React Router, Vue Router, etc.), the route resolves normally within the TWA.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Configuring TWA Deep Links<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your TWA&#39;s <code>AndroidManifest.xml<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n    android:name=&quot;com.example.twa.LauncherActivity&quot;\n    android:exported=&quot;true&quot;&gt;\n\n    &lt;!-- Default launch URL --&gt;\n    &lt;meta-data\n        android:name=&quot;android.support.customtabs.trusted.DEFAULT_URL&quot;\n        android:value=&quot;https:\/\/yourapp.com&quot; \/&gt;\n\n    &lt;!-- App Links for deep linking --&gt;\n    &lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n        &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n        &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n        &lt;data android:scheme=&quot;https&quot;\n              android:host=&quot;yourapp.com&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Coexistence Strategies<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 1: PWA Only (No Native App)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you only have a PWA (no native app), deep linking works through the browser. When the user installs the PWA (&quot;Add to Home Screen&quot;), the PWA opens when they tap its home screen icon, but links from other apps still open in Chrome.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To make links open the installed PWA instead of Chrome, wrap it in a TWA and publish to the Play Store. The TWA gives you App Links support.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 2: Native App Takes Priority<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most common pattern. Ship both a PWA and a native app. The native app handles all deep links via verified App Links. The PWA serves as the fallback for users who haven&#39;t installed the native app.<\/p>\n\n\n\n<pre><code>User has native app \u2192 link opens in native app\nUser has PWA only  \u2192 link opens in browser (PWA renders the page)\nUser has neither   \u2192 link opens in browser (PWA renders the page)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The PWA acts as a universal fallback because it&#39;s just a website. If the native app&#39;s App Links fail for any reason, the URL loads in the browser, and your PWA handles the rendering.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 3: TWA Wraps the PWA<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use a TWA to wrap your PWA and publish it on the Play Store. This gives you:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Play Store presence and discoverability<\/li>\n<li>App Links support (deep links open the TWA)<\/li>\n<li>The same codebase as your PWA (no separate native development)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The tradeoff: TWAs have limited access to native APIs compared to a fully native app. If you need push notifications, sensors, or Bluetooth, you&#39;ll need a native bridge or a full native app.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Strategy 4: Hybrid (Native Shell + Web Content)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use a native Android app that loads some screens via WebView (from your PWA&#39;s domain) and renders others natively. Deep links route to either the native screen or the WebView screen based on the path:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">fun handleDeepLink(uri: Uri) {\n    when {\n        \/\/ Native screens\n        uri.path?.startsWith(&quot;\/products&quot;) == true -&gt; openNativeProductScreen(uri)\n        uri.path?.startsWith(&quot;\/settings&quot;) == true -&gt; openNativeSettings()\n\n        \/\/ Web-based screens (loaded from PWA)\n        uri.path?.startsWith(&quot;\/help&quot;) == true -&gt; openWebView(uri.toString())\n        uri.path?.startsWith(&quot;\/legal&quot;) == true -&gt; openWebView(uri.toString())\n\n        \/\/ Default: native home\n        else -&gt; openNativeHome()\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Link Routing Between PWA and Native<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Detecting the Context<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Your PWA can detect whether it&#39;s running in a browser, installed as a standalone PWA, or inside a TWA:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Detect the display mode\nfunction getDisplayMode() {\n  if (document.referrer.includes(&#39;android-app:\/\/&#39;)) {\n    return &#39;twa&#39;; \/\/ Running inside a Trusted Web Activity\n  }\n  if (window.matchMedia(&#39;(display-mode: standalone)&#39;).matches) {\n    return &#39;standalone&#39;; \/\/ Installed PWA\n  }\n  return &#39;browser&#39;; \/\/ Regular browser tab\n}\n\n\/\/ Adjust behavior based on context\nconst mode = getDisplayMode();\nif (mode === &#39;browser&#39;) {\n  \/\/ Show &quot;Install our app&quot; banner\n  showInstallBanner();\n} else if (mode === &#39;twa&#39;) {\n  \/\/ Running as TWA; hide any &quot;open in app&quot; prompts\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Redirecting from PWA to Native App<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your PWA detects that the user should be using the native app (e.g., they need a feature only available natively), you can redirect:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Try to open the native app via intent URL\nfunction openNativeApp(path) {\n  const intentUrl = `intent:\/\/${path}#Intent;` +\n    `scheme=https;` +\n    `package=com.example.nativeapp;` +\n    `S.browser_fallback_url=${encodeURIComponent(window.location.href)};` +\n    `end`;\n\n  window.location.href = intentUrl;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Service Worker Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PWA service workers can interfere with deep linking by caching the wrong response for App Links verification requests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Never cache <code>\/.well-known\/assetlinks.json<\/code> in your service worker:<\/strong><\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ In your service worker\nself.addEventListener(&#39;fetch&#39;, (event) =&gt; {\n  const url = new URL(event.request.url);\n\n  \/\/ Don&#39;t cache Digital Asset Links or Apple App Site Association\n  if (url.pathname.startsWith(&#39;\/.well-known\/&#39;)) {\n    return; \/\/ Let the network handle it\n  }\n\n  \/\/ Cache everything else normally\n  event.respondWith(\n    caches.match(event.request).then(cached =&gt; cached || fetch(event.request))\n  );\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the service worker serves a cached (potentially outdated) <code>assetlinks.json<\/code>, App Links verification may fail.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku deep links<\/a> work with both PWAs and native apps. A single Tolinku URL resolves to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The native app (if installed and App Links are verified)<\/li>\n<li>The PWA (if the native app isn&#39;t installed, the link loads in the browser where your PWA renders)<\/li>\n<li>The app store (if configured as the fallback)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This means you don&#39;t need separate link strategies for PWA and native. One Tolinku link covers all scenarios. Configure your Android settings in the <a href=\"https:\/\/tolinku.com\/docs\/developer\/app-links\/\">Tolinku dashboard<\/a> for the native app, and your PWA naturally serves as the web fallback.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For Android Instant Apps (another alternative to PWAs), see the <a href=\"https:\/\/tolinku.com\/blog\/android-instant-apps-deep-links\/\">Instant Apps deep linking guide<\/a>. For the full App Links setup, see the <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">Android App Links guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Connect Progressive Web Apps to native Android apps with App Links. Handle TWA deep linking, manage the web-to-native transition, and configure both experiences.<\/p>\n","protected":false},"author":2,"featured_media":1282,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"App Links for Progressive Web Apps on Android","rank_math_description":"Connect Progressive Web Apps to native Android apps with App Links. Handle TWA deep linking and manage the web-to-native transition.","rank_math_focus_keyword":"app links PWA android","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-android-app-links-pwa.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-android-app-links-pwa.png","footnotes":""},"categories":[10],"tags":[25,23,20,69,326,327,41],"class_list":["post-1283","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","tag-android","tag-app-links","tag-deep-linking","tag-mobile-development","tag-pwa","tag-trusted-web-activity","tag-web-to-app"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1283","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=1283"}],"version-history":[{"count":5,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1283\/revisions"}],"predecessor-version":[{"id":2577,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1283\/revisions\/2577"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1282"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1283"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1283"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1283"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}