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.
What happens when a user taps a link
| Context | What opens the link | What to plan for |
|---|---|---|
| Android, PWA installed from Chrome | The WebAPK opens in-scope links in the installed app | Links outside scope open in the browser |
| Android, Trusted Web Activity wrapper | The wrapper app, through verified App Links | Digital Asset Links must match the signing key |
| Android, PWA added as a plain shortcut | The browser | No link capture at all |
| iOS, home-screen web app | Safari, not the home-screen app | Separate storage, so the user may not be signed in |
| iOS, native wrapper app | The app, through universal links | Associated domains entitlement and site file must agree |
Android: WebAPK and Trusted Web Activity
WebAPK link capture
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:
- The site publishes
/.well-known/assetlinks.jsonnaming 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. - 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.
iOS: Safari, home-screen apps and universal links
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 anapple-app-site-associationfile 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
- List the permitted route patterns and parameter types for each deep link. Reject anything else before rendering a sensitive screen.
- 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.
- Authorise the requested resource on the server. Holding a link never replaces an ownership check or a transaction confirmation.
- Never redirect to a URL taken from a parameter without matching it against an allowlist.
- 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.jsonmatches 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.