Flutter deep linking after Firebase Dynamic Links
firebase_dynamic_links is discontinued. Here's how to set up Universal Links and App Links in Flutter with go_router or app_links, test them, and handle deferred deep links.
Firebase Dynamic Links shut down on 25 August 2025, and old page.link URLs now return a 404. On pub.dev, the firebase_dynamic_links package is marked discontinued, and so is the older uni_links package (it points to app_links as its replacement).
If your Flutter app relied on Dynamic Links, you now need three things that FDL used to bundle together:
- Links that open the app: iOS Universal Links and Android App Links on a domain you control.
- Routing inside the app: turning
https://links.example.com/product/42into the right screen. - Deferred deep linking (optional): sending a new user to that same screen after they install from the store.
Flutter handles the first two well with no third-party service. The third needs one. This guide covers all three.
Step 1: Pick a link domain
Use a subdomain you control and don't use for anything else, like links.example.com. You'll host two small files there, and both platforms are strict about them (HTTPS, no redirects, JSON). A dedicated subdomain keeps your main website's redirects and firewall rules from getting in the way.
Step 2: Host the two verification files
iOS: https://links.example.com/.well-known/apple-app-site-association (no .json extension):
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.shop"],
"components": [{ "/": "/*" }]
}
]
}
}
ABCDE12345 is your Apple Team ID, followed by your bundle ID.
Android: https://links.example.com/.well-known/assetlinks.json:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.shop",
"sha256_cert_fingerprints": ["14:6D:E9:…:44:E5"]
}
}
]
List the SHA-256 of every key that signs builds you test with. For apps from Google Play, that's the Play App Signing key in Play Console, not your upload key. Get local fingerprints with cd android && ./gradlew signingReport.
Serve both files with Content-Type: application/json and no redirects.
Step 3: Native setup
iOS
In Xcode, open ios/Runner.xcworkspace, select the Runner target → Signing & Capabilities → + Capability → Associated Domains, and add:
applinks:links.example.com
No https://. One line per domain. Xcode writes this to Runner.entitlements.
Android
In android/app/src/main/AndroidManifest.xml, inside the MainActivity <activity>:
<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="links.example.com" />
</intent-filter>
Step 4: Route links in Dart (choose one approach)
There are two common ways to get the link into your app. Pick one, because they conflict.
Option A: Flutter's built-in deep linking + go_router
Flutter can hand incoming links straight to your router. Since Flutter 3.27 this is on by default, so there's nothing to add in Info.plist or the manifest. (On older Flutter versions, set FlutterDeepLinkingEnabled to true in Info.plist and add <meta-data android:name="flutter_deeplinking_enabled" android:value="true" /> to the activity.)
Define routes that match your link paths:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'product/:id',
builder: (context, state) => ProductScreen(
id: state.pathParameters['id']!,
ref: state.uri.queryParameters['ref'],
),
),
],
),
],
);
void main() => runApp(MaterialApp.router(routerConfig: router));
Tapping https://links.example.com/product/42?ref=share opens ProductScreen(id: '42', ref: 'share').
Good for: apps where every link maps cleanly to a route, with no logic in between.
Option B: the app_links package
If you need to inspect links before navigating (wait for login, call your API to resolve a short code, log analytics), use app_links (7.2.2 at the time of writing). First turn off Flutter's built-in handling, or both will handle the link. This is the step most people miss after upgrading to Flutter 3.27, when the default flipped to "on".
Info.plist:
<key>FlutterDeepLinkingEnabled</key>
<false/>
AndroidManifest.xml, inside <activity>:
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
Then listen for links. The stream delivers the link that launched the app as well as links that arrive while it's running:
import 'dart:async';
import 'package:app_links/app_links.dart';
class LinkHandler {
LinkHandler(this.router);
final GoRouter router;
final _appLinks = AppLinks(); // singleton; create it early to catch the cold-start link
StreamSubscription<Uri>? _sub;
Uri? _pending;
void start({required bool Function() isSignedIn}) {
_sub = _appLinks.uriLinkStream.listen((uri) {
if (uri.host != 'links.example.com') return;
final target = Uri(path: uri.path, queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters);
if (isSignedIn()) {
router.go(target.toString());
} else {
_pending = target; // open it after login
}
});
}
void afterLogin() {
final p = _pending;
_pending = null;
if (p != null) router.go(p.toString());
}
void dispose() => _sub?.cancel();
}
Good for: auth-gated screens, short links that need resolving, or analytics on every open.
Step 5: Test it
Build and install the app on a device or simulator, then:
# iOS Simulator
xcrun simctl openurl booted "https://links.example.com/product/42?ref=share"
# Android emulator or device
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://links.example.com/product/42?ref=share"
# Is the Android domain verified?
adb shell pm get-app-links com.example.shop
On a real iPhone, paste the link into Notes and tap it. Typing it into Safari's address bar never opens an app.
If it opens the browser instead, the problem is almost always the hosted files or the signing fingerprint, not your Dart code. Our guides for iOS Universal Links and Android App Links walk through each check, and the free Universal Link & App Link Validator checks both files, including Apple's CDN copy and Google's Digital Asset Links verdict.
Step 6: Deferred deep linking (what FDL did after install)
Universal Links and App Links only work when the app is already installed. When it isn't, the user goes to the store, installs, opens the app, and the original link is gone. Dynamic Links used to cover that gap. Neither Apple nor Google offers a built-in replacement. You have three options:
- An attribution or linking service with a Flutter SDK. Branch (
flutter_branch_sdk), AppsFlyer (appsflyer_sdk) and Adjust (adjust_sdk) all publish Flutter packages. They're built for marketing attribution and priced for it; check that the plan you're looking at includes deferred deep linking. - Build it yourself on Android with the Play Install Referrer. Put a click ID in the Play Store URL's
referrerparameter, then read it on first launch (for example with theandroid_play_install_referrerpackage) and ask your server which link it was. This is exact on Android. iOS has no equivalent, so iOS needs either a clipboard token (iOS shows a paste prompt) or probabilistic matching, which is harder to get right. - Skip it. If links mostly go to existing users (password resets, shared content, notifications), you may not need deferred links at all. Send new users to onboarding.
For the reasoning behind each approach, see What is deferred deep linking?.
Moving existing page.link URLs
Your old xyz.page.link links already return 404 and can't be redirected; that domain belonged to Firebase. What you can do:
- If you used your own custom domain with Dynamic Links (not
page.link), you still control it and can point it at a new link service or your own server. - Remove the old
page.linkhost from your Android intent filter and iOS Associated Domains. A leftover host that can't verify can stop all App Links verifying on Android 11 and earlier. - Recreate important links on your new domain with the same paths, and update the places that publish them: emails, QR codes, ads, push notifications.
- Keep your routes stable. If your new links use the same paths your app already routes, the Dart side doesn't change.
Firebase Dynamic Links has shut down: how to move your links in an afternoon covers exporting and recreating links in more detail.
Where CodeOnAir fits (and where it doesn't, yet)
CodeOnAir AppLinks gives you link domains (a free yourapp.codeonair.com subdomain, or your own) that already serve apple-app-site-association and assetlinks.json, built from the app details you enter. It also adds store fallbacks, social previews, QR codes and click analytics. Steps 1–5 above work the same with a CodeOnAir domain: add it to Associated Domains and your intent filter, and the files are already there.
To be clear about the limits: CodeOnAir's SDKs today are for React Native/Expo, Android (Kotlin) and iOS (Swift). There's no Flutter package yet, so deferred deep links and in-app link creation aren't one-line calls in Flutter. The SDK endpoints are plain REST, so a Flutter app can call them directly. If that's your case, request access and tell us you're on Flutter. AppLinks is on the paid plans (pricing). For the broader picture, see our Firebase Dynamic Links alternative page.
FAQ
What replaced firebase_dynamic_links in Flutter?
Nothing from Google. Use Universal Links and Android App Links on your own domain, with Flutter's built-in deep linking and go_router, or the app_links package. For deferred deep links, use a third-party service with a Flutter SDK, or build your own on the Play Install Referrer.
Why did app_links stop working after I upgraded Flutter?
Flutter 3.27 turned built-in deep linking on by default, and it conflicts with plugins like app_links. Set FlutterDeepLinkingEnabled to false in Info.plist and add <meta-data android:name="flutter_deeplinking_enabled" android:value="false" /> to your Android activity.
Should I use go_router's deep linking or app_links?
Use Flutter's built-in deep linking with go_router when every link maps directly to a route. Use app_links when you need to run logic first, such as waiting for login or resolving a short link through your API.
Do I need my own domain for Flutter deep links?
You need a domain where you can serve the two .well-known files over HTTPS without redirects. That can be your own subdomain, or a link domain from a hosted service.
How do I test Flutter deep links on Android?
Install the app, then run adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://your.domain/path". Check verification with adb shell pm get-app-links <package>; the domain should say verified.
Does flutter run work for testing App Links?
Yes. Debug builds are signed with your debug key, so add that key's SHA-256 to assetlinks.json while testing. Builds from Google Play need the Play App Signing key instead.
Checked 2026-10-22: Firebase Dynamic Links FAQ (shutdown 25 August 2025), docs.flutter.dev "Deep links flag change" and app links cookbook, pub.dev package status and versions. Package versions change often; check pub.dev before copying code.