skip to content

Your React Native app's recipe App Links open the app from a locally signed release build but open the browser once installed from Google Play; what is the likely cause?

level: seniorimportance: should knowfreq 35%

answer

  1. who signs the build users install?
  2. Play App Signing re-signs
  3. upload key versus app signing key
  4. list every fingerprint in assetlinks.json
  5. pm get-app-links shows the state

basics

~20 s

With Play App Signing, Google re-signs the app with the app signing key, so an assetlinks.json listing only the upload or local key's SHA-256 fingerprint no longer matches; add the app signing key's fingerprint from Play Console.

solid answer

~40 s

Android verifies App Links by comparing the installed app's **signing certificate** with the `sha256_cert_fingerprints` in `assetlinks.json`. Builds you sign locally or with EAS use your key, but apps installed from Google Play under **Play App Signing** are re-signed by Google with the **app signing key**, whose fingerprint differs from the **upload key**. If the file lists only the upload key, Play installs fail verification, and on Android 12+ the link silently opens the browser. The fix is to add the app signing key's SHA-256 fingerprint, shown in Play Console's app signing page, to the array, keeping the other fingerprints you still test with. Confirm on a device with `adb shell pm get-app-links com.example.recipes`, then re-run verification with `adb shell pm verify-app-links --re-verify com.example.recipes`.

go deeper

for a junior

Recall that assetlinks.json lists the app's signing-certificate fingerprints, and that a build signed with a different key will not verify.

for a middle

Explain the upload key versus app signing key split under Play App Signing and why the file must list the key that signed the installed APK.

for a senior

Diagnose with pm get-app-links, fix the fingerprint list for every signer, re-verify, and test a store-installed build rather than a local one.

for a principal

Make fingerprint lists part of the signing and credential change process across flavours and domains, so a key rotation cannot silently break every shared link.

## How Android decides a link is verified An App Link is an `https` intent filter marked `android:autoVerify="true"`. When the app is installed or updated, Android fetches `https://<host>/.well-known/assetlinks.json` for each host in those filters and checks two things against the **installed APK**: - the `package_name` equals the app's package, and - one of the `sha256_cert_fingerprints` equals the SHA-256 fingerprint of the certificate that **signed the installed app**. The second check is what makes App Links trustworthy: another developer can reuse your package name on a sideloaded build, but cannot sign with your key. It is also why the same app can verify in one install and fail in another. The file describes a *signer*, and different installs can have different signers. ## Why Play installs differ Apps on Google Play normally use **Play App Signing**. There are then two keys: | Key | Who holds it | Signs | |---|---|---| | **Upload key** | you, or EAS credentials | the bundle you upload, and builds you sign yourself | | **App signing key** | Google | the APKs Google Play delivers to users | Google strips your upload signature and re-signs with the app signing key. So the fingerprint of a Play-installed app is the **app signing key's**, not the key that signed your local release build. An `assetlinks.json` built from your local keystore, or from `eas credentials -p android`, which shows the fingerprint of the keystore EAS signs with, verifies your own builds and fails for every user who installed from Play. A related variant: debug builds are signed with the debug keystore, so App Links in a debug build only verify if that debug fingerprint is also listed. ## The fix 1. Open Play Console's app signing page and copy the **app signing key certificate** SHA-256 fingerprint (Play Console also offers a ready Digital Asset Links snippet). 2. Add it to `sha256_cert_fingerprints`, keeping the upload or EAS fingerprint if you test locally signed builds against the same domain. The field is an array precisely so several signers can be trusted. 3. Serve the file again over HTTPS, as `application/json`, with no redirect, on **every** host the intent filters name. 4. Re-run verification on a test device and check the result. ## Confirming on a device On Android 12 and later, `pm` exposes the verification state directly: ```bash adb shell pm get-app-links com.example.recipes adb shell pm verify-app-links --re-verify com.example.recipes ``` `get-app-links` lists each host with its state, for example `verified` or a failure state, so you can see which host failed instead of guessing. `verify-app-links --re-verify` asks the system to fetch the files again, which you need after changing `assetlinks.json` because verification otherwise happens at install. To fire a link at the app without a browser, the Expo docs use `adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://..."`. ## Why nobody noticed The failure is silent by design: - On **Android 12+**, a web link that is not verified for any app opens in the **default browser**; the user sees the website and assumes that is normal. - On **older Android versions**, an unverified `https` filter could still surface the app in a chooser dialog, which at least hinted at the problem. - The React Native side cannot help: `Linking.getInitialURL()` only runs if the app is opened, and here it never is. Other causes produce the same symptom and are worth ruling out with `get-app-links`: a host listed in the intent filter that does not serve the file, a redirect on `/.well-known`, or the wrong content type. Since Android 12 verification is tracked per host, so one broken host no longer disables the others; on older versions a single failing host could make the whole app unverified. ## What to take away Treat `assetlinks.json` as part of the release configuration. Whenever the signing setup changes (enabling Play App Signing, rotating the upload key, moving to EAS-managed credentials, adding a flavour with its own package), update the fingerprint list in the same change and verify a store-installed build, not only a local one.

  • Why can't you simply trust the package name in assetlinks.json and skip the fingerprint?
    A package name is not a secret: anyone can build and sideload an APK with `com.example.recipes`. The SHA-256 fingerprint ties the domain to whoever holds the signing key, so only genuine builds can claim the domain's links. Without it, a malicious app could intercept password-reset or magic links sent as `https` URLs.
  • Your Android App Links worked until you added a second host to the intent filter; what should you check first?
    Run `adb shell pm get-app-links` for the package and look at the new host's state. The new host must serve its own `/.well-known/assetlinks.json` over HTTPS with the right fingerprints and no redirect. On Android 12+ only that host fails; on older versions a single unverifiable host could leave the whole app unverified, so the old links would break too.

saying these in an interview costs you the question

  • The upload key fingerprint is what Play-installed apps are signed with
  • sha256_cert_fingerprints must hold exactly one fingerprint
  • Android re-fetches assetlinks.json each time a link is tapped
  • On Android 12+, a failed App Link still shows the app in a chooser
  • The fingerprint to list is the website's TLS certificate fingerprint
  • Editing assetlinks.json fixes already-installed apps immediately