Firebase Dynamic Links

Dynamic Links are Dead. What’s next?

Firebase is retiring the beloved Dynamic Links service in 2025. Why it is good news and what to do next — find out in this story.

Pavel Ryabov
Firebase Developers
13 min readAug 30, 2023

--

🔔 I recently released a Firebase Extension called FlowLinks that offers a powerful alternative to Dynamic Links. While it doesn’t fully replace FDL functionality, it features all the must-have features (that we discuss in this story) and can be installed and set up in just a few clicks.
Learn more about FlowLinks here.

Earlier this year, Firebase announced the termination of the Dynamic Links. Even though these links are set to go completely “404” on us only in 2025, it’s hard to imagine any developer wanting to use them at this point.

This sounds scary at first, given how developers have come to rely on Dynamic Links for easy in-app content sharing and app promotion.

You’ll be surprised to hear that this is actually good news! There are multiple great ways of implementing dynamic links using Firebase functionality without relying on the soon-to-be-deprecated service, and we will cover some of them in this blog. But first, let’s dive into the terminology.

🤔 What are… Dynamic Links?

The terminology for a seemingly simple feature of URIs that are able to open application content is surprisingly messed up. This is caused by platforms (iOS, Android, Firebase) calling their links different names, even though the difference between them lies only in implementation — they all, give or take, execute the same function. Let’s cover these terms one by one.

Deep Links

Deep links are a way to link directly to content within an app. They are URLs that are recognized by the app, and when clicked, they take the user directly to the content within the app. App Links work by using the Intent system on Android devices and Custom URL Schemes on iOS devices. Problem: These links can’t do anything if the app is not installed on your phone.

Sample Links:

  • Instagram: instagram://media?id=123456789
  • Spotify: spotify:track:123456789
  • Facebook: fb://page?id=123456789

Universal/App Links

Universal Links (iOS) and App Links (Android) use https scheme and require a specified host (domain name) and some additional setup. This approach is optimal for creating shareable app content, since the links can be handled many ways and will act like normal links when being shared online. Problem: Additional setup and a hosted domain are required.

Dynamic Links

Firebase Dynamic Links — so-called “deferred” links. They have the same functionality as Deep links while addressing their key limitation — Dynamic Links are resilient in scenarios where the corresponding application is not installed. If the app is not installed, the user will be forwarded to its page in the appropriate application store.
These links look like common URLs we’re used to seeing. Firebase allowed a pretty extensive configuration for these links. For example, you could decide what to open when the link is clicked on a Desktop or other unsupported device. Essentially, Dynamic Links remove the need for an additional backend setup for Universal/App Links.

An example of a Deferred (Dynamic) link

Under the hood

Technically, you can link any domain name to open their links in your app. That doesn’t sound very safe, does it? So thought the developers, and added the .well-known challenge for the hosting. Two JSON files (for Android and iOS accordingly) should be accessible on your hosting in order for your app to know that the links that start with your domain “consent” to be opened in the app. You can actually view those settings for any website!

Let’s try Facebook — just open https://facebook.com/.well-known/apple-app-site-association, and you’ll see a JSON file listing all of the “acknowledged” applications that Facebook uses.

There’s a cool website that allows you to view .well-known configs for any domain. Explore it to find different configurations!

⏩ Looking for something more simple?

For the rest of the blog, we will be covering the implementation of deep links using Cloud Functions, Express Node, and Firebase Hosting. This is a great solution for showing dynamic content. For example, it allows you to fetch a blog post or an article from Firestore and fill the page with dynamic meta tags and other content.

However, if you’re looking for a much easier solution, I also wrote a guide that only requires a Firebase Hosting setup. You can check it out here. The drawback of this approach is that you can only create static previews and content (via HTML paths), which won’t look great if used with dynamic content from your app.

Check out the example below!

Which one would you click on?

👑 Long live Flow Links!

Glad you stayed for some challenge. While Firebase Dynamic Links did offer a nice UI wrap for your links’ configuration, it’s not rocket science — you can set up your own links in just a couple of steps, which we will cover below. And there’s more — you’ll be able to further customize these links, providing users with a preview of your app’s content and making the appeal of clicking on the link as big as possible.

Under the hood, we will be using the Universal and App Links, mentioned above. In order to reduce confusion, I will be calling our implementation Flow Links. Why? Because these links are all about you creating a flow that will guide the user to the app based on device and content type.

Flow Links have all these features and more!

📝 What do we need?

Firebase Cloud Functions and Firebase Hosting. That’s it! Some simple TypeScript logic will help us create a flow for any shareable links in our application.

Recommended setup

Every application needs a nice landing page that talks about the features and provides installation links. I will assume that you already have it, if not — create it, it’s essential!

From now on, let’s assume your main landing page is accessible via the domain “myapp.com”. I would strongly recommend separating in-app links and your main website logic. This will allow you to separate Web Previews and other shareable content with your landing page. For example, Spotify uses a subdomain “open” for sharing links. When clicked on a desktop, it opens the web player, and on mobile, it opens the mobile app.

📝 Tip: Good subdomain names would be open, app, preview, and share.

Webpage content

Just like Firebase Dynamic Links, our deep links will open a page in the application if it is installed and will open something else if it’s missing on the device. That “something else” can be multiple things, so let’s consider our options:

  1. Redirect to an Application Store. A blank page that will redirect users to the application’s page in App Store/Google Play. This is the “lazy” solution that doesn’t engage people enough to download the app and will not work well on the Web.
  2. Application landing page. Not the best solution, since the users expect to see some content after clicking on a link, but it’s better than sending them directly to the app store, especially if the link is opened on an unsupported device.
  3. Content Web preview. The best solution — provide users with a glimpse of your app’s content, and implement solutions that will engage them to download the app (i.e. restrict some functions like bookmarking, commenting, or reading more without downloading the app).

🛠️ Let’s do it!

Now that we have an outline for what our deep links should do, time to start implementing them. We will create a simple Cloud Function that will process an HTTPS request and return a webpage with dynamic meta tags.

Initialize your NodeJS project

  1. Create a new Firebase project or use an existing one.
  2. Enable Cloud Functions and Hosting features in Firebase Console.
  3. Install Firebase CLI:
npm install -g firebase-tools

4. If not already logged in, authenticate by running

firebase login

5. Initialize Firebase by running the command below. During the setup process, make sure to select both Functions and Hosting. Then, use the created Firebase project.

firebase init

Next, select TypeScript, disable ESLint, and install dependencies with npm.

Set up the Hosting:

6. Make sure to install the needed packages:

cd functions && npm add firebase-functions firebase-admin express fs path --save && npm install

Time to code!

Open the index.ts file and remove all generated code.

  1. Import express, functions, and admin packages at the top of the file.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import * as express from 'express';
import * as fs from "fs";
import * as path from 'path';
  1. Set up the Firebase app and make Express handle all Cloud Functions.
// Initialize Firebase Admin SDK
admin.initializeApp();
// Initialize Express app
const app = express();
// Set up Firebase Cloud Function
export const api = functions.https.onRequest(app);

Now all created Express routes will be automatically converted into Cloud Functions. Let’s create a wildcard endpoint to handle all incoming requests.

app.get('*', (req, res, next) => {
// Code will be here...
});

Webpage

Webpage… so, why do we need it?! It’s simple — users need to see something when they open the link from a device that is either unsupported (Desktop, TV, etc.) or doesn’t have the app installed. Also, this allows us to add any kind of sweet services — Google Analytics, redirection to a Store, app banner, you name it!

From this point, there is an infinite number of ways you can process the link request. For example, if you have a Firestore collection called articles and share an entry from it. Image your shared link being https://.../articles/rF45yhgFS88ffs4. You can fetch the shared document from the articles collection by using URL parameters and get the article metadata (title, summary, image, etc.) Then, you can pass it to the HTML page to achieve a beautifully-looking link preview.

For easy thumbnail image (og:image) creation, you can use this extension made by Invertase. Check out a tutorial on how to use it here.

Another example might be a referral link — you can validate the referral code with Firestore, present the user with a page showing their discount, and offer to download the app to redeem it.

To keep things simple, I’ll show a template implementation that you can improve on your own.

app.get('*', (req, res, next) => {
// Define values
const title = 'My Amazing Application';
const subtitle = 'Find out more about my app...';
const image = 'https://.../your-app-banner.jpg';

// Load HTML template
const templatePath = path.join(__dirname, './assets/html/index.html');

// Replace handles with content
var source = fs.readFileSync(templatePath, { encoding: 'utf-8' })
.replaceAll('{{title}}', title)
.replaceAll('{{subtitle}}', subtitle)
.replaceAll('{{image}}', image);

// Return the webpage
return res.send(source);
});

📝 Tip: if your IDE shows a code error saying “Property ‘replaceAll’ does not exist on type ‘string’.”, replace the string “es2017” with “es2021” in your tsconfig.json file.

Make a folder named “assets” within the “/lib” directory.
Inside the “assets” folder, create another folder named “html.”
Create the index.html file in the /functions/lib/assets/html folder.

We have to put all of our static assets in the lib folder since it contains compiled JS files and TypeScript compiler will otherwise ignore all non-ts content when building the project.

We will be using simple handlebars to make our meta tags dynamic. Here’s an example of what the index.html file might look like:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="apple-itunes-app" content="app-id=myAppID, affiliate-data=myAffiliateData, app-argument=myURL">

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta name="apple-mobile-web-app-title" content="My Awesome App">
<meta name="apple-mobile-web-app-title" content="My Awesome App">

<link rel="icon" type="image/png" href="...">
<link rel="mask-icon" href="" color="#ffffff">
<meta name="application-name" content="My Awesome App">

<title>{{title}}</title>
<meta name="description" content="{{subtitle}}" />
<meta property="og:title" content="{{title}}" />
<meta property="og:description" content="{{subtitle}}" />
<meta property="og:image" content="{{imageUrl}}" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />

<meta name="twitter:card" content="summary_large_image"></meta>
<meta name="twitter:title" content="{{title}}"></meta>
<meta name="twitter:site" content="myawesomeapp.com"></meta>
<meta name="twitter:description" content="{{subtitle}}"></meta>
<meta name="twitter:image" content="{{imageUrl}}"></meta>

<link rel="apple-touch-icon" href="...">
</head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Detect.js/2.2.2/detect.min.js" rossorigin="anonymous" referrerpolicy="no-referrer"></script>

<body>
<!-- YOUR PAGE CONTENT HERE -->
</body>

<script>
// Optional: redirect users on mobile platforms to the according store
var result = detect.parse(navigator.userAgent);
if (result.os.family === 'iOS') {
window.location.href = 'https://apps.apple.com/us/app/YOUR_APP_ID';
} else if (result.os.family.includes('Android')) {
window.location.href = 'https://play.google.com/store/apps/details?id=YOUR_APP_ID';
}
// You can handle any other logic here - Google Analytics, popups, etc...
</script>
</html>

Make sure to fully set up this webpage, adding all possible meta tags, favicons, and decorations.

Configure .well-known URIs

Add the code below before the wildcard endpoint. It will show to Apple servers that your backend “consents” to open links with its host name in your application.

app.get('/.well-known/apple-app-site-association', (req, res) => {
const applicationID = `${APPLE_TEAM_ID}.${IOS_BUNDLE_ID}`
// Example: FGSV552D.com.example.myawesomeapp

res.writeHead(200, { 'Content-Type': 'application/json' })
res.write(JSON.stringify({
"applinks": {
"apps": [],
"details": [{
"appID": applicationID,
"paths": [
"*",
],
}]
},
"webcredentials": {
"apps": [
applicationID
]
}
}))
res.end();
});

The setup for Android is similar. You can test your implementation here. Read more about how to produce SHA-256 fingerprints in this official guide.

app.get('/.well-known/assetlinks.json', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.write(JSON.stringify(
[{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "YOUR_PACKAGE_NAME",
"sha256_cert_fingerprints": [
"YOUR_SHA256_FINGERPRINT"
]
}
}]
))
res.end();
});

Firebase Hosting

There’s just one problem left — our Cloud Function is available only through the “api” path that we defined in index.ts. Without setting up Hosting, your links will look weird: for example,

https://region-project-id.cloudfunctions.net/api/your-content/content_ID

is quite bulky for a shareable link, isn’t it?

Let’s fix it! This solution will allow anyone to access the links using the Firebase Hosting domain name (or a linked custom domain).

In the firebase.json file, modify the "hosting" configuration to look like this:

...
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"appAssociation": "NONE", // Override .well-known config
"rewrites": [
{
"source": "**", // Match all routes
"function": "api" // Our Cloud Function name
}
]
}
...

That’s it for the backend part! Deploy it using the command:

firebase deploy --only functions,hosting

You can see the full index.ts file below:

Test your implementation by accessing these URLs:

https://YOUR_HOST/wildcard_testing
https://YOUR_HOST/.well-known/apple-app-site-association
https://YOUR_HOST/.well-known/assetlinks.json

Now, time to set up our app to accept Flow Links. I will cover the native implementation for iOS and Android.

📱 Frontend (Application) Setup

Android

Add the lines below to your app manifest. For a more in-depth tutorial, follow this Ultimate Guide.

📝 Tip: make sure to specify a relevant domain name based on your config (custom domain, Hosting domain or Functions domain)

<manifest ...>
<!-- ... -->
<application ...>
<activity ...>
<!-- App Links -->
<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" />
<!-- Accepts URIs that begin with https://YOUR_HOST -->
<data
android:scheme="https"
android:host="[YOUR_HOST]" />
</intent-filter>
</activity>
</application>
</manifest>

iOS

You need to add or create a com.apple.developer.associated-domains entitlement - either through Xcode (see below) or by editing (or creating and adding to Xcode) ios/Runner/Runner.entitlements file.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ... -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:[YOUR_HOST]</string>
</array>
<!-- ... -->
</dict>
</plist>

This allows for your app to be started from https://YOUR_HOST links.

Creating the entitlements file in Xcode:

  1. Launch Xcode by double-clicking on your ios/Runner.xcworkspace file.
  2. Go to the Project navigator and choose the Runner root item at the top.
  3. Select the Runner target, and then open the Signing & Capabilities tab.
  4. Click the + Capability button to add a new capability.
  5. Type ‘associated domains’ and pick the corresponding option.
  6. Double-click the initial entry in the Domains list and modify it from webcredentials:example.com to “applinks: + your host” (for example, applinks:my-app.com)
  7. This action generates Runner.entitlements file which will be automatically included in the project.

For more information, check out Apple’s guide for Universal Links.

Further implementation

Further link processing fully depends on the framework you’re using. To avoid oversaturating this blog, I will leave you with some good and well-tested solutions for handling universal links: Flutter, React Native, Native Android, and Native iOS.

Conclusion

Today we covered the death of Dynamic Links, discussed different link types, and got under the hood of the technology to see how it really works. We learned how to create Flow Links — a powerful alternative for Dynamic Links that carry nearly the same functionality and can do even more.

Note that some features of Dynamic Links, such as deferred links, Google Analytics, and Short URLs are not covered by Flow Links.

Share your thoughts on Flow Links in the comments and offer your use cases for this technology!

📣 Follow me on Twitter (𝕏) to keep in touch and receive a daily dose of Firebase and Flutter blogs, as well as tips and tricks.

More articles

--

--