Shipping a PWA to the Google Play Store With a TWA
A Trusted Web Activity puts your existing web app on Play with no port and no second codebase. What Bubblewrap generates, why Digital Asset Links is the step everyone gets wrong, and what Play actually reviews.
The usual assumption is that getting on the Play Store means building an Android app. For a progressive web app it doesn't. A Trusted Web Activity is a thin Android wrapper that opens your live website full-screen, with no browser UI, using the user's installed Chrome as the engine.
There is nothing to port. The site the browser loads is what runs inside the app. You keep deploying to your host; the store listing just points at it.
What a TWA actually is
A TWA is Chrome Custom Tabs with the browser chrome removed — no address bar, no toolbar. Because it's the real browser, you get the real web platform: your service worker, your caches, your storage, all the same as in a tab. Users on the store version and users on the web share one implementation and one set of bugs.
The removal of the address bar is the part that has to be earned. Anyone could otherwise ship an app that opens your bank's website full-screen. So Chrome requires you to prove that the app and the website are controlled by the same person, using Digital Asset Links. Get that wrong and the app still works — it just shows a URL bar across the top, which looks broken and is the single most common TWA complaint.
Prerequisites
- A live HTTPS URL serving the PWA.
- A PWA that meets the installability bar: a web manifest with
name, icons including maskable 192 and 512,display: standalone, a registered service worker, served over HTTPS. - A Play Console account (one-time $25).
- Node 18+ and JDK 17 for the build.
Verify installability first in Chrome DevTools → Application → Manifest. If Chrome won't offer to install it, wrapping it won't help.
Generating the wrapper
Bubblewrap reads your manifest and generates the Android project:
npm install -g @bubblewrap/cli
bubblewrap init --manifest https://games.shresthaco.com/manifest.webmanifest
bubblewrap buildinit asks for a package name (reverse-domain, e.g. com.shresthaco.games,
permanent once published), app name, colours, and signing key details. build
produces an .aab for the store and an .apk for local testing.
Install the APK on a real device before going near the Play Console:
adb install app-release-signed.apkIf you see an address bar, asset links aren't right yet. Fix that before uploading anything.
Digital Asset Links — the step that goes wrong
You publish a file at https://<your-domain>/.well-known/assetlinks.json
declaring which Android app you trust:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.shresthaco.games",
"sha256_cert_fingerprints": ["AB:CD:EF:..."]
}
}
]Three things reliably break this.
The fingerprint must be from the key Play actually signs with. This is the
one that catches almost everyone. If you use Play App Signing — and you should,
and it's mandatory for new apps — Google re-signs your upload with their key.
The fingerprint from your local keystore is then the wrong one. Take it from
Play Console → Setup → App integrity → App signing key certificate, not
from your local keytool output.
It must be served as application/json from the exact path, over HTTPS,
with no redirect. A host that redirects /.well-known/* or serves it as
text/plain will fail silently.
It has to be on the domain in your manifest's start_url. If your PWA
lives on a subdomain, the file goes on that subdomain.
Verify with Google's checker rather than by eye:
https://digitalassetlinks.googleapis.com/v1/statements:list
?source.web.site=https://games.shresthaco.com
&relation=delegate_permission/common.handle_all_urls
Then uninstall and reinstall the app — Chrome caches the verification result, so a fixed asset links file won't take effect on an existing install.
What Play actually reviews
The listing needs the usual assets: icon, feature graphic, screenshots per form factor, short and full descriptions.
Two policy points matter more for a TWA than for a native app.
Minimum functionality. Play rejects apps that are "just a website". A TWA passes when it's a genuine app experience — offline capability, standalone display, real functionality. This is another reason the service worker isn't optional: an app that shows a network error when offline reads as a webview wrapper, and gets rejected as one.
The Data Safety form. You declare what you collect, and it's cross-checked against your privacy policy. If your app genuinely collects nothing, say so — "no data collected" is a legitimate and easily-defended answer, and it's much simpler than the alternative. What you must not do is paste a generic privacy policy claiming you collect personal information you don't touch. An inaccurate declaration is a policy violation in the direction people don't expect.
What you get, and what you give up
You get: store discovery and installs, a listing people can be pointed to, Android's share and intent handling, and — the real win — the ability to ship a fix by deploying. The store version updates when the user's Chrome next fetches your site. There's no review queue between a bug and its fix, and no version fragmentation, because there's only one version.
You give up: native APIs the web can't reach, iOS (a TWA is Android only — on iOS you're still on Add to Home Screen), and a first launch that depends on the network if your service worker hasn't installed yet.
For a PWA that's already built and already good, the cost of getting onto Play is an afternoon of configuration and $25 — not a second codebase. That trade is hard to argue with.