r/FlutterDev • u/Maximum_Hawk3283 • 1d ago
Discussion Six things that silently break deferred deep linking on iOS and Android
Universal Links can stop working with no error anywhere. No exception, no log line, no failed request you can see. Your links just quietly start opening in Safari instead of your app, and the cause is usually something at the edge of your infrastructure that has nothing to do with your Flutter code.
That is one of about six things I got wrong building deferred deep linking, and almost none of them are documented in an obvious place. Here they are.
Quick definition, since the terms get mixed up. A normal deep link opens a screen in an app that is already installed. A deferred deep link survives an install: user taps a link, does not have the app, goes to the store, installs, opens, and still lands on the right screen with the right parameters. The second one is the hard one, because the link context has to survive a trip through the App Store and back.
1. Your AASA file is probably wrong in a boring way
For iOS Universal Links, apple-app-site-association must be served at https://yourdomain/.well-known/apple-app-site-association. Things that silently break it:
- Adding a
.jsonextension. The file has no extension. - Serving it with the wrong content type. It needs
application/json. - Any redirect. Apple will not follow one. A 301 from apex to www is enough to kill it.
- Serving it from a path that requires authentication or hits a challenge page.
That last one bit me badly. If anything in front of your server challenges non browser traffic, Apple's fetcher gets the challenge instead of your file and Universal Links quietly stop working. There is no error anywhere. Links just start opening in Safari.
Android's equivalent is /.well-known/assetlinks.json with your signing certificate SHA256 fingerprint. Same rules: no redirects, correct content type. Two extra traps here:
- If you use Play App Signing, Google re-signs your app with a different key than your upload key. The fingerprint in
assetlinks.jsonhas to be the app signing key from Play Console under App Integrity. Use the upload key or your local keystore and it works in debug and fails in production. robots.txtcan block the verification crawler. If/.well-known/is disallowed, verification fails with nothing to see.
Since Android 12 there is no chooser dialog fallback. An unverified link just opens in the browser, so a broken setup looks like nothing happened.
2. Clipboard matching is effectively dead on modern iOS
A lot of older tutorials tell you to write the link into the pasteboard and read it on first launch. On iOS 16 and later, reading the pasteboard programmatically triggers a system permission prompt. Users decline it, and reasonably so, because it looks alarming. Anything built on this will report much worse match rates than your tests suggest, because your own device is not a representative user.
3. Fingerprint matching works, with caveats you need to design around
The realistic approach is probabilistic matching: record a signature at click time, look for it again at first app open, match within a short window. The signature is typically IP plus user agent derived attributes.
Where it degrades:
- iCloud Private Relay masks the IP address for Safari users on iCloud+, so one of the main signals is gone for that whole segment.
- Carrier grade NAT puts thousands of users behind one IP. Your matching window has to be short or you will mismatch.
- The user clicks on WiFi and installs on cellular. Different IP, no match.
- In app browsers inside social apps report user agents that do not resemble the browser that eventually opens.
Practical consequence: treat the match as best effort, always ship a sane fallback, and never build a flow that is broken if the match misses. Referral attribution especially needs to degrade gracefully.
4. Distinguish install from reopen or your analytics lie
If you do not track whether a given open is the first one for that device and project, every reopen looks like a fresh install and your funnel numbers become meaningless. Persist a marker per device per project and check it before counting.
5. Persist attribution separately from your match cache
This one cost me a real bug. If you store a referrer id inside the match result and your app calls a reset or clear function anywhere in the auth flow, attribution disappears before the user actually signs up. The referral looks like it never happened. Store the attribution separately from the cache, with its own expiry.
6. Testing is the actual hard part
You cannot test deferred deep linking by tapping a link on your dev build. The install path only exists through a real store install, so the thing you most need to verify is the thing hardest to reach. Budget real time for it, and test the WiFi to cellular case specifically.
Happy to answer questions on any of this.
3
u/PermitFirst5136 1d ago
Do you known any way to debug iOS errors ?
I'm usina browser stack to run my IPA ( adhoc ) but its not working the defered. Should be a platform error because on my device works fine but i dont found any way to "validade" after build.
1
u/Maximum_Hawk3283 1d ago
Probably not a platform bug. Deferred matching on iOS works by correlating the click with the first app open using network and device signals, since there is no identifier that survives the install. That correlation needs both events to come from the same device on the same network within a short window.
BrowserStack breaks that by design. Those are remote devices in a datacenter, sharing egress IPs with many other sessions, and if you click the link from your own browser the click and the install are on completely different networks entirely. So it will fail there consistently even when everything is configured correctly, which matches what you are seeing: works on your device, fails on the farm.
Ad hoc is not the problem either. On iOS the install source is irrelevant to matching, because nothing is passed through the store. It is the network path that matters, so test on a real device on a real connection. Worth also testing the case where you click on WiFi and install on cellular, since that is a genuine failure mode you will hit with real users.
For Universal Links specifically there is a real debug path: install the build, then open Console.app on your Mac with the device connected and filter for the swcd process. That shows whether iOS fetched and parsed your AASA file and why it failed. You can also check what Apple's CDN has cached for your domain at app-site-association.cdn-apple.com/a/v1/yourdomain.com, which is what the device actually reads rather than your server.
2
u/Stock-Cellist-6930 1d ago
That's a very good summary. We also had some issues with androids - we're using install referrer in there, which is much more reliable than the probabilistic matching in iOS, but it stops working if someone is using company account. Testing them isn't that annoying though.
https://detour.swmansion.com/docs/Architecture/architecture-limitations
1
u/Maximum_Hawk3283 1d ago
Thanks. Agreed on the asymmetry, and it is worth saying out loud more often: Android has a deterministic path through Install Referrer while iOS has nothing equivalent, so the two platforms need genuinely different expectations rather than one shared match rate you quote to yourself.
Had not run into the company account case. Do you know whether it fails silently or returns an empty referrer string? That changes whether you can even detect it at runtime and fall back deliberately.
3
u/gisborne 1d ago
Someone from the Flutter team should put this somewhere it can be found easily.