Replacing Firebase Dynamic Links with App Links and Universal Links in Flutter (2026 Indie Guide)
Firebase Dynamic Links shut down on 2025-08-25. If your Flutter app still has page.link URLs in old emails, QR codes, or share sheets, those links now return 404, and any code that waits for Dynamic Links data simply gets nothing back. Plenty of small apps never finished the migration because the old links “mostly worked” right up to the deadline. This guide is the version we wish we had: what Dynamic Links actually did for you, which pieces you really need to replace, how to serve the two verification files from Firebase Hosting, the Flutter-side setup on Android and iOS, and a test matrix to run before every release. It builds on our Flutter + Firebase shipping checklist and the Firebase Auth hardening guide.
What actually broke, and what you need back
The Firebase Dynamic Links deprecation FAQ spells out the post-shutdown behavior. Clicked links return HTTP 404, the short-link and link-stats APIs return 400 or 403, and first-open attribution calls return empty data rather than crashing. The SDKs don’t crash your app, but your own code has to handle the empty responses.
Dynamic Links bundled several jobs into one product. Split them apart before you pick a replacement:
- Open the installed app from a normal https link. This is what most indie apps used it for. Android App Links and Apple Universal Links do it natively, on your own domain, for free.
- Route to the right screen. Your Flutter router handles this once the link reaches the app.
- Deferred deep linking. Carrying context through a store install, so a new user lands on the shared item after first launch. App Links and Universal Links don’t do this on their own. Firebase’s FAQ points teams that need full parity to third-party deep-linking vendors.
- Short links and click analytics. Short URLs on your own domain are easy to serve. Click analytics can come from your web fallback page or from Google Analytics.
Be honest about deferred deep linking. For a solo app with a referral flow, a vendor might be worth it. For most utility apps, “open the app if installed, otherwise show a good web page with store buttons” covers nearly every real user.
Don’t forget email links in Firebase Auth
If you use email link sign-in, password reset, or email verification links that open the app, the FAQ notes that these keep working only with updated Firebase Authentication SDKs. Older app versions that still depend on Dynamic Links lose those flows. Follow the current email link authentication guide for Flutter, then raise your minimum supported version if a meaningful share of users is still on old builds. A Remote Config prompt, as in our kill switch and staged rollout guide, is a gentle way to push those users forward.
The two verification files
Both platforms prove that your app and your domain belong together through a JSON file at a fixed path on that domain.
Android: assetlinks.json. Android’s assetlinks configuration guide requires the file at https://yourdomain/.well-known/assetlinks.json, served with content type application/json, over HTTPS, and without any 301 or 302 redirects. Each host your app handles needs its own copy. The file lists your package name and the SHA-256 fingerprint of the signing certificate. This is the most common failure for solo developers. If you use Play App Signing, the fingerprint from your local keystore won’t match what users install. Take the fingerprint, or the ready-made JSON snippet, from Play Console under Release, then Setup, then App signing. Add your debug fingerprint too if you test with debug builds.
iOS: apple-app-site-association. Apple’s Supporting associated domains page says the file has no extension, lives in .well-known, and must be served over HTTPS with a valid certificate and no redirects. It lists your app ID (Team ID plus bundle ID) and the URL paths the app should claim. Since iOS 14, devices don’t fetch the file from your server. Apple’s CDN does, typically within 24 hours, and devices check for updates about once a week after install. A broken file can therefore stay broken on users’ phones for days, so test before you ship.
Serving the files from Firebase Hosting
Firebase Hosting works well for this, as long as you account for its rules. According to the Hosting configuration reference, Hosting resolves a request in this order: reserved /__/* paths, configured redirects, exact-match static content, configured rewrites, then 404 pages. In practice:
- Put both files in your public directory under
.well-known/. Exact-match static files win over your single-page app’s catch-all rewrite. - Check your redirects. Redirects run before static content. A broad rule, such as forcing a trailing slash or sending the apex domain to
www, can break verification. Serve the files on the exact host that appears in your links. - Set the content type for the extensionless Apple file with a
headersrule:
"headers": [{
"source": "/.well-known/apple-app-site-association",
"headers": [{"key": "Content-Type", "value": "application/json"}]
}]
- Remove leftover Dynamic Links rewrites. Old projects often carry
"dynamicLinks": truerewrites infirebase.json. Hosting’sappAssociationsetting defaults toAUTO, which lets Hosting generate these files dynamically for Dynamic Links. After you deploy your own files, fetch both URLs and confirm the response contains your package name and Team ID, not something generated. - Check that your public directory includes dot-folders. Hosting can’t serve
.well-knownif yourignoreglobs or build step leave it out.
The Flutter side
Flutter’s deep linking overview and its cookbooks for Android App Links and iOS Universal Links cover the setup. The parts that trip people up:
- Android manifest. Add an intent filter with
android:autoVerify="true", theVIEWaction, theDEFAULTandBROWSABLEcategories, and anhttpsdata element with your host. Keep test hosts out of release builds. Android’s guide notes that on Android 11 and lower, verification needs every listed host to pass. - The deep linking flag. Flutter 3.27 changed the default of its built-in deep linking flag to on. Flutter’s deep links flag change note says apps using a plugin such as
app_linksor a vendor SDK must setflutter_deeplinking_enabled(Android) andFlutterDeepLinkingEnabled(iOS) to false, or two handlers will compete for the same link. - iOS entitlement. Add the Associated Domains capability in Xcode with
applinks:yourdomain. Keep the paths in your Apple file in sync with the routes your app actually handles. - Routing. With a router such as go_router, each link path maps to a screen. Send unknown paths to a safe default rather than an error page. Treat link parameters as untrusted input, and check sign-in state before opening anything private.
A test matrix before every release
- Fetch both files with
curl -Ion the exact host. Expect 200,application/json, and no 3xx. - Re-verify on Android. On Android 12 or later, run
adb shell pm verify-app-links --re-verifywith your package name, thenadb shell pm get-app-links, and look forverified. The verify App Links guide lists the other states. - Cold start. With the app fully closed, tap a link from a notes app or email. It should open the right screen, not the home screen.
- Warm start. With the app already open on another screen, tap a link and confirm navigation.
- Not installed. Uninstall and tap a link. Your web page should load with useful content and store buttons.
- Signed out. Tap a private link while signed out. The app should ask for sign-in, then continue to the linked screen.
Keep a short list of legacy page.link URLs you know are still in circulation, such as old QR codes or onboarding emails, and replace them at the source. They will never resolve again.
Cleanup after the migration
- Remove the Dynamic Links SDK and any code that waits for its data.
- Delete leftover URL prefixes in the Firebase console, as the FAQ recommends.
- Add the two
.well-knownURLs to the uptime checks you already run for your site. Our App Check staging guide describes a matching staging routine.
Key takeaways
- Dynamic Links is gone. Old links return 404, so fix them at the source.
- App Links and Universal Links cover “open the installed app” on your own domain. Deferred deep linking needs a vendor or a deliberate decision to skip it.
- Most failures come from the files: wrong fingerprint, redirects, wrong content type, or the wrong host.
- Turn off Flutter’s built-in handler if a plugin handles links.
- Run the six-step test matrix before every release that touches routing or hosting.
Sources
- Firebase: Dynamic Links Deprecation FAQ
- Android Developers: Configure website associations (assetlinks.json)
- Apple Developer: Supporting associated domains
- Firebase: Configure Hosting behavior
- Flutter: Deep links flag change
More from gspteck: Cloud Functions secrets and rotation.