On this page
- scope
- prerequisites
- phase 1 — inventory your FDL usage
- phase 2 — stand up AASA and assetlinks.json
- phase 3 — implement universal-link / app-link handling
- phase 4 — redirect inbound FDL URLs
- phase 5 — handle deferred deep linking, or accept the loss
- phase 6 — remove FDL dependencies
- phase 7 — validation
- migration timeline
- related dev reading
- references
scope
Step-by-step migration from Firebase Dynamic Links to a working replacement. Assumes you have read /guides/firebase-dynamic-links-replacement and chosen one of the four target architectures. This guide is the execution playbook.
The general post-FDL strategic context lives at the hub: /guides/firebase-dynamic-links-replacement. The consumer-facing manifestation of similar link-failure problems lives at the thesis: /guides/in-app-browser-logged-out.
prerequisites
Before starting the migration:
- A domain you control with DNS access and the ability to serve static files over HTTPS.
- App Store / Play Store identifiers (Team ID + bundle ID for iOS, package name + SHA-256 cert fingerprint for Android).
- Read/write access to your
AppDelegate.swiftandApplication.kt(or equivalent). - A staging environment with a TestFlight build and an internal-track Play release.
- A test plan covering: cold install, warm relaunch, link tap from email, link tap from social-app webview, link tap from notification.
phase 1 — inventory your FDL usage
Audit existing code before changing anything.
# iOS
grep -r "DynamicLinks" ios/ --include="*.swift" --include="*.m"
grep -r "FirebaseDynamicLinks" ios/Podfile
grep -r "page.link" ios/
# Android
grep -r "FirebaseDynamicLinks" android/ --include="*.kt" --include="*.java"
grep -r "firebase-dynamic-links" android/app/build.gradle
grep -r "page.link" android/
# Server-side / web
grep -r "page.link" web/ src/
Classify each callsite:
| Pattern | What it indicates |
|---|---|
DynamicLinks.dynamicLinks().handleUniversalLink(...) |
iOS universal-link handling — replace with native application(_:continue:) |
getDynamicLink(intent) (Android) |
App-launch handling — replace with native Intent.getData() |
pendingDynamicLink first-launch read |
Deferred deep linking — needs an attribution replacement |
DynamicLinkComponents builder |
Server-side or client-side link generation — replace with your own URL builder |
Any URL of form yourapp.page.link/* in stored content |
Inbound traffic to redirect post-shutdown |
The deferred-deep-linking callsites are the hard ones. Mechanism: /guides/deferred-deep-linking-explained.
phase 2 — stand up AASA and assetlinks.json
Replace FDL's hosted association files with your own. Detailed format documents: /guides/aasa-file-explained and /guides/assetlinks-json-explained. Setup walkthroughs: /guides/ios-universal-links-setup and /guides/android-app-links-setup.
The shape:
iOS — https://yourdomain.com/.well-known/apple-app-site-association
{
"applinks": {
"details": [
{
"appIDs": ["ABCD1234EF.com.yourcompany.yourapp"],
"components": [
{
"/": "/l/*",
"comment": "Matches short-link routes"
},
{
"/": "/share/*",
"comment": "Matches share-card routes"
}
]
}
]
}
}
Served with Content-Type: application/json, no extension, no redirects, on the apex or www per your associated-domains entitlement.
Android — https://yourdomain.com/.well-known/assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
]
}
}]
Verify before proceeding:
# AASA
curl -sI https://yourdomain.com/.well-known/apple-app-site-association | grep -i content-type
# Expected: application/json; charset=utf-8
# assetlinks.json
curl -s https://digitalassetlinks.googleapis.com/v1/statements:list \
?source.web.site=https://yourdomain.com \
&relation=delegate_permission/common.handle_all_urls
Apple caches AASA aggressively. After the first publish, a device may need a full app reinstall to pick up changes. Use the Apple swcutil tool on a development device to verify the file was fetched.
phase 3 — implement universal-link / app-link handling
Replace FDL's handler with the platform-native primitives.
iOS — AppDelegate.swift
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return false
}
// route by path
let path = components.path
if path.hasPrefix("/l/") {
return routeShortLink(slug: String(path.dropFirst(3)))
}
return false
}
Android — MainActivity.kt
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val data: Uri? = intent.data
if (data != null && data.pathSegments.firstOrNull() == "l") {
val slug = data.pathSegments.getOrNull(1) ?: return
routeShortLink(slug)
}
}
For Android, register the intent filter in AndroidManifest.xml:
<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="yourdomain.com" android:pathPrefix="/l/" />
</intent-filter>
The autoVerify="true" flag is what makes this an App Link rather than a plain deep link. The semantic distinction matters: /guides/universal-link-vs-deep-link-vs-app-link.
phase 4 — redirect inbound FDL URLs
Even though FDL no longer resolves, inbound traffic to yourapp.page.link/xyz URLs continues for months — pasted in old emails, screenshots, embedded media. Capture it.
You no longer control the *.page.link domain. Google's hosted DNS for that subdomain is dead. Inbound traffic to it can't be redirected by you.
What you can redirect: any link that was rendered through your own infrastructure that referenced *.page.link. Find and replace them in:
- Email templates
- In-app share-sheet output
- Web property URLs
- Documentation, support articles, marketing site
For each, point to https://yourdomain.com/l/:slug directly. Slug encoding should match what your new router expects.
If you logged FDL slug → destination mappings in your analytics or your backend, dump that mapping and serve it from your new router so older inbound URLs still resolve to the intended destination.
phase 5 — handle deferred deep linking, or accept the loss
The hardest job to migrate. See /guides/deferred-deep-linking-explained for the full mechanism.
If you're using Branch/AppsFlyer/Adjust: integrate per vendor SDK. Test the cold-install path on a real device — simulators don't reproduce the install-referrer flow.
If you're building your own:
- Android: register a
BroadcastReceiverforINSTALL_REFERRERand parse the referrer string. The Play Store passes it through reliably. - iOS: there is no reliable path post-ATT. Best-effort approaches: server-set clipboard handoff before redirect (Apple-restricted), SKAdNetwork conversion postbacks (lossy, aggregated), or a "tap to confirm" first-launch UX that asks the user to re-tap a link from email.
The honest answer: iOS deferred deep linking in 2026 is a degraded product. Plan your install funnel to not depend on it where possible. Pages on adjacent SaaS as references: /guides/branch-io-alternative, /guides/appsflyer-onelink-alternative, /guides/adjust-deep-links-alternative.
phase 6 — remove FDL dependencies
Once the replacement is live and verified in production:
iOS
# Podfile — remove
pod 'Firebase/DynamicLinks'
// AppDelegate.swift — remove
import FirebaseDynamicLinks
FirebaseApp.configure() // keep if other Firebase services used
DynamicLinks.dynamicLinks().handleUniversalLink(...)
Run pod install and verify the framework is no longer linked.
Android
// app/build.gradle — remove
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
// MainActivity.kt — remove
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
FirebaseDynamicLinks.getInstance().getDynamicLink(intent)
Re-run Gradle sync and verify the dependency is gone from app/build/intermediates/.../merged_manifests/.
Firebase console
Remove the Dynamic Links configuration from your Firebase project. The console still shows the FDL section for legacy reasons; it does nothing.
phase 7 — validation
End-to-end smoke tests:
| Test | Expected behavior |
|---|---|
Tap https://yourdomain.com/l/foo in iOS Mail |
iOS app opens, route to foo destination |
| Tap same URL in mobile Safari with app installed | iOS app opens, route to foo |
| Tap same URL in mobile Safari with app NOT installed | Fallback web page renders |
| Tap same URL in Instagram in-app browser | Universal Link behavior is suppressed in in-app webviews; fallback web page renders. See /guides/in-app-browser-cookies-explained. |
| Cold install from store, return to original link destination | Deferred deep linking — depends on SDK integration |
Tap https://yourdomain.com/l/foo on Android Chrome |
Android app opens, route to foo |
| Tap on Android with app NOT installed, install from Play Store | First launch routes to foo via install-referrer |
The in-app-browser failure mode in row 4 is intentional in the platform spec — Universal Links don't trigger inside WKWebView instances. Apps must use universal link fallback handling and the user-side escape is the in-app browser escape flow.
migration timeline
A small app (single product, single platform) can complete the technical work in 3–5 engineering days plus 1 week of staged rollout. A large app with paid-acquisition attribution dependencies should plan 3–6 weeks including marketing-team validation.
Critical path items:
- AASA + assetlinks.json publish + DNS/TLS validation: 1–2 days
- iOS/Android handler implementation + test: 2–3 days
- SDK integration (if SaaS path): 2–5 days
- Inbound URL redirect mapping: 1 day
- Validation on real devices across iOS/Android matrix: 3–5 days
- Production rollout with feature flag: 1 week
related dev reading
- The strategic decision: /guides/firebase-dynamic-links-replacement
- iOS setup: /guides/ios-universal-links-setup
- Android setup: /guides/android-app-links-setup
- Deferred deep links specifically: /guides/deferred-deep-linking-explained
- Universal vs deep vs app link: /guides/universal-link-vs-deep-link-vs-app-link
- Bridge to consumer-side problem: /guides/in-app-browser-logged-out
If the non-app side of your link infrastructure — bio links, campaign URLs, social-share routing — is part of this migration, the no-SDK linkboo path lives at link.boo/api and link.boo/docs.
references
- Firebase Dynamic Links FAQ, deprecation timeline October 2023 — August 2025
- Apple Supporting Universal Links documentation
- Android App Links Verification documentation
- Play Install Referrer Library documentation
- Apple App Tracking Transparency framework documentation