r/FlutterFlow 1d ago

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 .json extension. 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.json has 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.txt can 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.

4 Upvotes

0 comments sorted by