skip to content

In a React Native app, what must the website and the native projects configure so iOS Universal Links and Android App Links verify?

level: middleimportance: must knowfreq 55%

answer

  1. both sides must name each other
  2. applinks: entitlement, no https://
  3. TeamID.bundleID in the AASA
  4. autoVerify plus VIEW, DEFAULT, BROWSABLE
  5. package_name and sha256_cert_fingerprints

basics

~10 s

The domain hosts /.well-known/apple-app-site-association (Team ID plus bundle ID, paths) and /.well-known/assetlinks.json (package name, SHA-256 signing fingerprints) over HTTPS; the app declares applinks:domain in Associated Domains and an https intent filter with autoVerify.

solid answer

~40 s

Verification is a **two-way association**. On the website, iOS needs `/.well-known/apple-app-site-association`, a JSON file without an extension whose `applinks` section lists the app as `<TeamID>.<bundleID>` and the paths it handles; Android needs `/.well-known/assetlinks.json` with the `delegate_permission/common.handle_all_urls` relation, the `package_name` and the `sha256_cert_fingerprints` of every key that signs the app. Both must be served over HTTPS with a valid certificate and no redirects. In the app, iOS needs the Associated Domains entitlement `com.apple.developer.associated-domains` containing `applinks:recipes.example.com` (host only, no `https://`), and Android needs an intent filter with action `VIEW`, categories `DEFAULT` and `BROWSABLE`, the `https` scheme and host, and `android:autoVerify="true"`. In an Expo project those come from `ios.associatedDomains` and `android.intentFilters` in app config.

code

xml · 8 lines
xml
<activity android:name=".MainActivity" android:launchMode="singleTask" android:exported="true">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="recipes.example.com" android:pathPrefix="/r" />
  </intent-filter>
</activity>

go deeper

for a junior

Recall the two file names under /.well-known and that the app must list the domain too; both sides have to agree.

for a middle

Explain what each file identifies (Team ID plus bundle ID versus package plus signing fingerprint), the applinks: entitlement format and the autoVerify intent filter, and how Expo generates them.

for a senior

Point out the serving rules that break verification in practice: redirects, missing hosts, wrong fingerprints, and that neither OS re-checks the file on every tap.

for a principal

Decide how many domains and subdomains the app should claim and who owns the verification files, since every claimed host becomes an operational dependency of the mobile release.

## Why two sides An app cannot prove it owns a domain by claiming it, and a website cannot force an app to open. So both platforms ask each side to **name the other**: the app lists the domain, and the domain lists the app. The OS fetches the website's file and compares it with the installed app. Only a match makes the `https` URL open the app. This is the **two-way association**, and every piece below is one half of it. ## The website half Serve both files under `/.well-known/` on **every host** the app claims, so `recipes.example.com` and `www.example.com` each need their own copy. | | iOS | Android | |---|---|---| | Path | `/.well-known/apple-app-site-association` (no `.json` extension) | `/.well-known/assetlinks.json` | | Identifies the app by | `<TeamID>.<bundleID>` in `appID` or `appIDs` | `package_name` plus `sha256_cert_fingerprints` | | Also lists | which paths open the app (`paths`, or `components` with `exclude`) | nothing about paths; paths live in the intent filter | | Serving rules | HTTPS, valid certificate, no redirects, at most 128 KB uncompressed | HTTPS, valid certificate, no redirects, `Content-Type: application/json` | Key details: - The **Team ID** is the Apple developer team that signs the app. A mismatch with the provisioning profile's team is enough to break verification. - The **fingerprints** are SHA-256 hashes of the app's **signing certificate**, not of the website's TLS certificate. List every key that signs a build users install: release, and if you test App Links in development, the debug keystore too. - On iOS the paths are part of the website file, so the website decides which URLs open the app; `/r/*` can open the app while `/blog/*` stays on the web. ## The app half **iOS.** The app must be signed with the Associated Domains capability, and its entitlements must contain `com.apple.developer.associated-domains` with entries such as `applinks:recipes.example.com`. The value is a bare host: writing `https://` inside the entry is a well-known mistake that silently disables Universal Links. In a bare React Native app you add the capability in Xcode; the provisioning profile must include it too. **Android.** `AndroidManifest.xml` needs an intent filter on the activity that hosts React Native, normally `MainActivity`: 1. action `android.intent.action.VIEW`; 2. categories `DEFAULT` and `BROWSABLE`; 3. `<data>` with `android:scheme="https"`, the `android:host` and optionally a path prefix; 4. `android:autoVerify="true"` on the filter, which tells Android to fetch `assetlinks.json` for its hosts and, on success, make the app the verified handler. **Expo.** With Continuous Native Generation the same halves come from app config: `ios.associatedDomains` becomes the entitlement, and an `android.intentFilters` entry with `autoVerify: true` becomes the manifest filter at prebuild. EAS Build also registers the Associated Domains capability with Apple for you. ## Common mistakes that break the association Most broken setups fail on one of a handful of details, and each one fails silently: - **Protocol in the entitlement**: `applinks:https://recipes.example.com` instead of the bare host. - **Wrong identifier in the AASA**: a staging bundle ID, or a Team ID from a different developer team than the one that signs the build. - **Wrong fingerprint in `assetlinks.json`**: the upload key or a local keystore listed, while users install builds signed by a different key. - **A redirect on `/.well-known/`**: for example the apex domain redirecting to `www`, or a login wall in front of the file. - **A missing host**: the intent filter or entitlement names `www.example.com` and `recipes.example.com`, but only one of them serves the files. - **Expecting instant effect**: editing a file on the server does not update devices that already verified at install time. ## When the OS checks Verification is not done on every tap. iOS fetches the association file when the app is installed or updated and refreshes it only occasionally after that; Android verifies when the app is installed or updated. A change to either file therefore does not reach existing installs immediately, which matters when you debug. ## What React Native does afterwards Once verification succeeds, the incoming URL is just a URL. React Native delivers it through `Linking`: `Linking.getInitialURL()` for a cold start and the `'url'` event for a running app. On iOS a bare project's `AppDelegate` forwards the universal-link activity to `RCTLinkingManager`; on Android the URL arrives in the `VIEW` intent. The same JavaScript handles recipe links whether they came from a scheme or a verified domain.

  • Why does the iOS file list paths while the Android file does not?
    On iOS the website decides which paths open the app, through `paths` or `components` in the AASA, including `exclude` rules. On Android, `assetlinks.json` only proves the domain trusts the app; the paths the app accepts are declared in the manifest's intent filter `<data>` elements. So adding a new app path on iOS is a website change, while on Android it is an app change.
  • In an Expo project, how do you declare the app half without editing native files?
    Put `applinks:recipes.example.com` in `ios.associatedDomains` and add an `android.intentFilters` entry with `action: "VIEW"`, `autoVerify: true`, `category: ["BROWSABLE", "DEFAULT"]` and a `data` entry with `scheme: "https"` and the host. Prebuild writes the entitlement and the manifest filter, and EAS Build registers the Associated Domains capability with Apple.

It works like a reference check: the app's entitlement or manifest says 'I work for recipes.example.com', and the OS phones the domain's /.well-known file to confirm. If the domain does not name the app back, the claim is ignored.

saying these in an interview costs you the question

  • The entitlement entry should be applinks:https://recipes.example.com
  • sha256_cert_fingerprints holds the website's TLS certificate fingerprint
  • Only the apex domain needs the file; subdomains inherit it
  • The AASA file must be named apple-app-site-association.json
  • Redirecting /.well-known to a CDN host is fine because the OS follows redirects
  • autoVerify is optional; Android verifies every https filter anyway