Skip to content

Distribute a PWA via app stores

Published

At the end of this guide the same site is listed on Google Play as a Trusted Web Activity (TWA) and on the Microsoft Store as a packaged PWA, both generated from the live manifest rather than rewritten, and the web version stops advertising an install to users who already have the store build. A TWA renders your origin full screen in the user’s browser, so the web experience is the product; the store package is a shell plus a verified link to the site.

You need an installable PWA on a domain you control, Node.js 14.15.0 or later for the Bubblewrap CLI (JDK 17 and the Android command-line tools are downloaded on first run if absent), a Google Play developer account, and a personal Microsoft account enrolled in the Windows Developer Program.

Generate the Android project with Bubblewrap

Section titled “Generate the Android project with Bubblewrap”

Trusted Web Activity is available in Chrome on Android 72 and later; where Chrome lacks TWA support it falls back to a Custom Tab with a toolbar. Bubblewrap reads the manifest, asks you to confirm each value it copies into the Android project, and builds a signed APK:

  1. Install the CLI (without sudo) and initialise from the manifest URL.

    Terminal window
    npm i -g @bubblewrap/cli
    bubblewrap init --manifest=https://notes.example/manifest.webmanifest

    init creates a signing key during the prompts; keep the keystore and its passwords, as this becomes the upload key.

  2. Build and install the result on a connected device.

    Terminal window
    bubblewrap build
    bubblewrap install # or: adb install app-release-signed.apk

    The build writes app-release-signed.apk. On first launch the site opens as a Custom Tab with the URL bar visible; that is expected until Digital Asset Links verification passes.

  3. Confirm which browser is providing the activity.

    Terminal window
    adb logcat -v brief | grep -e TWAProviderPicker

Run bubblewrap doctor if the build fails: it checks that the JDK and Android SDK recorded in ~/.bubblewrap/config.json exist and are the expected versions.

The URL bar disappears only when the site vouches for the app. Serve this file at https://notes.example/.well-known/assetlinks.json; Android’s App Links documentation requires Content-Type: application/json, HTTPS, and no 301 or 302 redirects:

[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "example.notes.twa",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}]

The fingerprint must be the certificate of the key that signs the APK users receive. Google Play requires Play App Signing for AAB uploads, which means Google re-signs the app with an “app signing key” that differs from the Bubblewrap upload key; Android’s documentation notes that a keytool fingerprint from your local keystore “will usually not match the one on users’ devices” in that case. Copy the SHA-256 of the “App signing key certificate” from Play Console under Release > Setup > App integrity, and list both the upload and the app signing fingerprints so test installs and Play installs both verify. A mismatched or missing fingerprint silently downgrades the TWA to a Custom Tab with browser UI on top.

Package for the Microsoft Store with PWA Builder

Section titled “Package for the Microsoft Store with PWA Builder”

Microsoft documents that no code changes are needed: reserve the name in Partner Center, package with PWA Builder, submit. In Partner Center choose Apps and games > New product > MSIX or PWA app, enter a name and select Reserve product name, then open Product management > Product Identity and copy Package ID, Publisher ID, and Publisher display name. At pwabuilder.com enter the site URL under Ship your PWA to app stores, click Start, fix anything listed under Action Items, then Package For Stores > Windows > Generate Package, paste the three values, and Download Package. The zip contains an .msixbundle and a .classic.appxbundle; upload both on the Packages step of Start your submission. Microsoft states review typically completes within 24 to 48 hours. Front-end and service-worker changes reach store users without a resubmission; a manifest change (icon, name, file_handlers, share_target) requires a new package because the manifest is copied into it.

Hide the install banner once the store app is present

Section titled “Hide the install banner once the store app is present”

navigator.getInstalledRelatedApps() resolves with the related apps the user has installed and is the API MDN names for removing “install our app” banners. It requires a top-level secure context (an <iframe> throws InvalidStateError) and is not Baseline, so default to showing the banner when the method is missing:

async function shouldShowInstallBanner() {
if (!('getInstalledRelatedApps' in navigator)) return true;
const related = await navigator.getInstalledRelatedApps();
return related.length === 0;
}

The call only finds apps declared in the manifest’s related_applications and whose own side points back: the Android app through the Digital Asset Links file above, a Windows app through URI handlers, and a PWA through a self-referencing related_applications entry.

{
"related_applications": [
{ "platform": "play", "id": "example.notes.twa", "url": "https://play.google.com/store/apps/details?id=example.notes.twa" },
{ "platform": "windows", "id": "12345Publisher.FieldNotes_abcdef123456" }
]
}

Leave prefer_related_applications absent or false; setting it to true on Android sends users to Google Play instead of installing the web app.

← Back to the Guides overview.