Technical guide

How to build a secure iOS and Android PWA with deep links

Make PWA deep links safe on Android and iOS: WebAPK and Trusted Web Activity link capture, Safari behaviour, manifest scope, launch_handler and route checks.

  • Platforms differ

    Android can open links in an installed PWA. iOS opens them in Safari. Design each journey for what the platform really does.

  • Links are input

    Validate every route and parameter, and authorise the action on the server, however the link arrived.

  • Release together

    Version the manifest, association files, service worker and any wrapper app as one release.

A deep link into a PWA behaves differently on each platform, so design the journey per platform and treat every link as untrusted input. On Android, Chrome can open in-scope links in an installed PWA through its WebAPK, and a Trusted Web Activity wrapper captures links through verified App Links. On iOS, universal links never open a home-screen web app: they open a native app if one claims the domain, otherwise Safari. Set the manifest scope and launch_handler deliberately, validate routes and parameters, and authorise every action on the server.

For the product context, see progressive web app protection.

ContextWhat opens the linkWhat to plan for
Android, PWA installed from ChromeThe WebAPK opens in-scope links in the installed appLinks outside scope open in the browser
Android, Trusted Web Activity wrapperThe wrapper app, through verified App LinksDigital Asset Links must match the signing key
Android, PWA added as a plain shortcutThe browserNo link capture at all
iOS, home-screen web appSafari, not the home-screen appSeparate storage, so the user may not be signed in
iOS, native wrapper appThe app, through universal linksAssociated domains entitlement and site file must agree

Android: WebAPK and Trusted Web Activity

When a user installs a PWA from Chrome on Android, Chrome generates a WebAPK: a small Android package that registers intent filters for URLs inside the manifest scope. Tapping an in-scope link then opens the installed PWA instead of a browser tab. Other browsers may install a plain shortcut with no link capture, so test the browsers your users actually use.

Keep scope as narrow as the app needs. A scope of / captures every link on the origin, including help pages and marketing links you may want in the browser.

Trusted Web Activity

A Trusted Web Activity wraps your PWA in an Android app that renders it full screen in the user’s browser. Two declarations must agree:

  1. The site publishes /.well-known/assetlinks.json naming the app’s package and the SHA-256 fingerprint of its signing certificate. With Play App Signing, use the fingerprint of the app signing key from Play Console, not your upload key.
  2. The app declares the site, and an intent filter with android:autoVerify="true" for the hosts it should open.

If verification fails, the TWA shows browser UI with a URL bar, like a Custom Tab. On current Android, unverified web links open in the browser by default rather than in the app. Check verification state on a test device with adb shell pm get-app-links <package>, and use pm verify-app-links --re-verify <package> after changing the site file.

Universal links work only for native apps. A home-screen web app does not register for them, so a tapped link opens Safari, even when the web app is installed. Home-screen web apps on iOS also keep their storage separate from Safari, so a user signed in to the home-screen app arrives in Safari signed out.

Design for that:

  • Make every deep-link route work in Safari on its own, with a clear sign-in step that returns to the original route.
  • Do not rely on cookies or local storage set inside the home-screen app.
  • If you need links to open an app, ship a native wrapper with the applinks: associated domains entitlement and an apple-app-site-association file at /.well-known/ on each domain. Each subdomain needs its own entry. Limit the paths the app claims.

Manifest scope and launch_handler

scope defines which URLs belong to the installed app. Navigation outside it shows browser UI or opens a browser, which is your signal that the user has left the app’s boundary.

launch_handler controls what happens when a link launches an installed PWA that is already open. Its client_mode can be navigate-new, navigate-existing, focus-existing or auto. With focus-existing, the app receives the target URL through window.launchQueue.setConsumer and must route it itself, so validate that URL like any other input. Support is limited to Chromium-based browsers and differs by platform, so check current support before relying on it.

{
  "id": "/",
  "start_url": "/app/",
  "scope": "/app/",
  "launch_handler": { "client_mode": "focus-existing" }
}

Validate every route

  1. List the permitted route patterns and parameter types for each deep link. Reject anything else before rendering a sensitive screen.
  2. Keep credentials, personal data and long-lived tokens out of URLs. Use a short-lived, single-use code exchanged server-side where a link must carry authority.
  3. Authorise the requested resource on the server. Holding a link never replaces an ownership check or a transaction confirmation.
  4. Never redirect to a URL taken from a parameter without matching it against an allowlist.
  5. Do not let the service worker answer an authenticated navigation from cache. Cache the public app shell; fetch account data and transaction results fresh.

Otukan protects the native wrapper of a web app packaged as an Android or iOS store app, and fits your existing build and release pipeline while signing keys stay with you. A browser-only PWA has no native wrapper to protect. Content handling is covered on controlling PWA uploads and downloads.

Test and release

  • Each deep link works when opened cold, signed out, signed in and with an expired session.
  • Android: link capture checked for the WebAPK, the TWA (if used) and a plain browser.
  • Android: assetlinks.json matches the Play app signing key; verification state is “verified”.
  • iOS: links opened from Mail, Messages and a web page land in Safari or the wrapper as designed.
  • Malformed parameters, unknown hosts and open-redirect attempts are refused.
  • Logout and account switching clear cached account data.
  • An old service worker with new assets, and the reverse, both behave correctly.
  • Rollback covers assets, manifest, association files and backend compatibility.

Further reading

Common questions

  • Can a universal link open a home-screen web app on iOS?

    No. Universal links work only for native apps. A home-screen web app does not register for them, so a tapped link opens Safari even when the web app is installed. Home-screen web apps also keep their storage separate from Safari, so the user may arrive signed out.

  • How does an installed PWA capture links on Android?

    Through its WebAPK when installed from Chrome. The WebAPK registers intent filters for URLs inside the manifest scope, so in-scope links open the installed PWA. Other browsers may install a plain shortcut with no link capture, and a Trusted Web Activity wrapper captures links through verified App Links instead.

  • Why does my Trusted Web Activity show a URL bar?

    Because verification failed. The site's /.well-known/assetlinks.json must name the app's package and the SHA-256 fingerprint of its signing certificate; with Play App Signing, use the app signing key from Play Console, not your upload key. Check the state with adb shell pm get-app-links <package>.

  • What changes when launch_handler uses focus-existing?

    The open app receives the target URL through window.launchQueue.setConsumer and must route it itself, so validate that URL like any other input. Support for launch_handler is limited to Chromium-based browsers and differs by platform, so check current support before relying on it.

  • Does Otukan protect a browser-only PWA?

    No. Otukan protects the native wrapper of a web app packaged as an Android or iOS store app, and a browser-only PWA has no native wrapper to protect. Otukan fits your existing build and release pipeline while signing keys stay with you.

Verify the entire link journey

Tell us your PWA framework, target browsers, any native wrapper and the deep-link journey. We do not ask for binaries, signing keys, credentials or production data.

Or request a technical demo