The product is a consumer mobile app built with Expo. Production lives on the store. Staging exists so QA can test reset-password, magic-share, and campaign landing flows before release. Deep links are how email and marketing URLs open a specific screen inside the app. I had two binaries but one shared link host. Both builds claimed the same Universal Link. iOS picked one, and it picked production. Staging did not work. Production opened paths it did not know.
QA taps a staging email and the store app launches. They get a blank screen, or yesterday's production route. A customer who taps a marketing link can land in an internal build that asks them to log into a fake catalog. That last one is how you get a one-star review that mentions "test." The screen team spent days on reset UX while the OS was opening the wrong binary.
One host is not two apps
I reused the production associated domain on the staging Expo profile because it is the same path. Apple does not care that your EAS profile is named staging. The AASA file on that host lists app ids. If both bundle ids are there, or if only production is there, the wrong binary wins.
Custom schemes had the same mess. myapp://reset on both flavors means whichever app is installed handles it. On a device with both, iOS picks one. Universal Links and custom schemes fail the same way when the claim is shared. iOS does not read your EAS profile name. It reads the AASA file and the scheme registrations.
Split the host, the scheme, and the bundle id
I gave staging its own scheme, bundle id, package name, associated domain, and Android host.
{
"expo": {
"scheme": "myapp-staging",
"ios": {
"bundleIdentifier": "com.example.app.staging",
"associatedDomains": ["applinks:staging.example.com"]
},
"android": {
"package": "com.example.app.staging",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [{ "scheme": "https", "host": "staging.example.com" }]
}
]
}
}
}Production keeps myapp, the store bundle id, and example.com. Staging emails and Klaviyo test flows have to emit the staging host. If Klaviyo still renders example.com in a test campaign, QA keeps launching the store app no matter how correct app.json looks. AASA on each host lists only that host's app id.
Android is the same claim with different files. package plus intentFilters host have to match the staging host, and autoVerify is how Play checks the assetlinks file. Reuse the production package name and a staging link can still open the store build. I have watched QA debug a "broken" reset screen that was just the production binary, opened from a staging mail, staring at a route the store app does not ship.
Keep staging out of production AASA
I stopped putting staging bundle ids in the production AASA. A leftover QA install can hijack a customer link while the store app sits right there.
If a staging reset looks broken, I check which binary iOS actually opened before I rewrite the screen. A blank production route is often a staging link that never reached the staging app. Deep links are a namespace. Each binary gets its own host, scheme, and bundle id, and marketing has to send the URL that matches the build under test.
Happy coding! Sander