← Back to list

Build Deep Links for React Native (Firebase Alternative)

Step-by-step tutorial for implementing custom deep links in Expo , handles installs and in-app navigation

Samuel Nnaemeka Onyeji · 2025-11-06 17:27 · 3 claps · 6.1 min read
#firebasedynamiclinks #deeplink #expo #react-native #firebase
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📱 · Mobile Development

Build Deep Links for React Native (Firebase Alternative)

Step-by-step tutorial for implementing custom deep links in Expo , handles installs and in-app navigation

Firebase Dynamic Links was a powerful tool that let developers create smart links that worked across platforms, whether or not users had the app installed. But when it shut down on August 25th, 2025, I found myself facing a choice: pay for a SaaS solution or build my own simple server to handle dynamic linking.

I decided to build my own, and in this tutorial, I’ll show you how to create your own dynamic links for your React Native app using Expo.

Here’s what we’ll build:

  • When users don’t have your app: The link redirects them to the App Store (iOS) or Play Store (Android
  • When the app is already installed: The link opens the app and navigates directly to the specific content

Create Simple Server

To get started, we’ll build our server using NestJS and TypeScript. Run these commands in your terminal:

$ npm i -g @nestjs/cli
$ nest new simple-server --strict

Once everything is installed, we’ll create a middleware that redirects users to the appropriate app store based on their device.

Create a new file in the src folder called os-redirect.middleware.ts:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class OsRedirectMiddleware implements NestMiddleware {
  private readonly IOS_LINK = 'https://ios-playstore-link';
  private readonly ANDROID_LINK = 'https://android-playstore-link';

  use(req: Request, res: Response) {
    const userAgent = req.headers['user-agent'] || '';

    const isIOS = /iPhone|iPad|iPod/i.test(userAgent);
    const isMac = /Macintosh|Mac OS X/i.test(userAgent);

    // Mac users are redirected to iOS store since many apps support both
    if (isIOS || isMac) {
      return res.redirect(302, this.IOS_LINK);
    }

    // Default to Android for all other devices
    return res.redirect(302, this.ANDROID_LINK);
  }
}

Now update your app.module.ts to apply this middleware to all routes:

import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';
import { OsRedirectMiddleware } from './os-redirect.middleware';

@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(OsRedirectMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

Finally, verify your main.ts looks like this:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

What we’ve accomplished: Every link that hits the server now automatically redirects users to the App Store or Play Store based on their device.

Next, we’ll connect our App to the server. By adding .well-known configuration files, we enable deep linking, when users click a link from your domain, their device will know to open your app if it's installed.

Setting Up the Well-known Folder Structure

In the root of your NestJS project, create the following structure:

your-project/ └─ public/ └── .well-known/

iOS Configuration (Universal Links)

In the .well-known folder, create a file called **apple-app-site-association** (note: no file extension).

Add the following content:

{
  "applinks": {
    "details": [
      {
        "appID": "TEAM_ID.BUNDLE_IDENTIFIER",
        "paths": ["*"]
      }
    ]
  }
}

Finding your credentials:

  1. Bundle Identifier: Found in your React Native project’s app.config.js under ios.bundleIdentifier. It looks like com.companyname.appname
  2. Team ID: Found in your Apple Developer account. This is a 10-character string (e.g., A1B2C3D4E5)
  3. Final format: A1B2C3D4E5.com.companyname.appname

APP ID

APP ID

Note: You’ll need to register your app identifier in App Store Connect before you can see your Team ID.

Android Configuration (App Links)

In the same .well-known folder, create a file called **assetlinks.json**.

Add the following content:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.companyname.appname",
      "sha256_cert_fingerprints": [
        "EAS_SHA256_FINGERPRINT",
        "PLAY_STORE_SHA256_FINGERPRINT"
      ]
    }
  }
]

Finding your credentials:

  1. Package Name: Found in your app.config.js under android.package. It looks similar to your iOS bundle identifier.
  2. EAS SHA-256 Certificate (first fingerprint):
  • Navigate to your app on the Expo dashboard
  • Go to CredentialsAndroid
  • Under Build Credentials, find Android upload keystore
  • Copy the SHA-256 Fingerprint

EAS SHA-256

EAS SHA-256

3. Play Store SHA-256 Certificate (second fingerprint):

  • Upload your build to Play Store (you can use internal testing)
  • Navigate to ReleaseSetupApp Integrity
  • Scroll to App signing , Click Settings.
  • Copy the SHA-256 certificate fingerprint

Google Play SHA-256 certificate fingerprint

Google Play SHA-256 certificate fingerprint

Note: EAS-managed Android builds are signed twice — once by Expo during build, and once by Google Play on upload. Both fingerprints are required.

Serving Your .well-known Files

By default, the middleware setup handles redirects but your server also needs to serve the .well-known files that iOS and Android check for deep linking.

Install the required package:

$ npm install @nestjs/serve-static

Edit your app.module.ts to include static file serving:

import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';
import { OsRedirectMiddleware } from './os-redirect.middleware';
import { join } from 'path';
import { ServeStaticModule } from '@nestjs/serve-static';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(process.cwd(), 'public'),
      serveRoot: '/',
      serveStaticOptions: {
        dotfiles: 'allow',
      },
    }),
  ],
  controllers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(OsRedirectMiddleware)
      .exclude({ path: '.well-known/*', method: RequestMethod.ALL })
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

Important: We’ve updated the middleware to exclude .well-known routes so the configuration files can be accessed directly without redirection.

This ensures your .well-known files are served correctly from the public folder.

Hosting on Platforms Like Vercel

Some platforms (like Vercel) don’t automatically serve dotfiles. In that case, create a custom controller to serve your .well-known files manually.

In the src folder, create a file called wellknown.controller.ts:

import { Controller, Get, Res } from '@nestjs/common';
import express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';

@Controller('.well-known')
export class WellKnownController {
  @Get('apple-app-site-association')
  getAppleAppSiteAssociation(@Res() res: express.Response) {
    const filePath = join(
      process.cwd(),
      'public',
      '.well-known',
      'apple-app-site-association',
    );
    const fileContent = readFileSync(filePath, 'utf8');
    res.setHeader('Content-Type', 'application/json').send(fileContent);
  }

  @Get('assetlinks.json')
  getAssetLinks(@Res() res: express.Response) {
    const filePath = join(
      process.cwd(),
      'public',
      '.well-known',
      'assetlinks.json',
    );
    const fileContent = readFileSync(filePath, 'utf8');
    res.setHeader('Content-Type', 'application/json').send(fileContent);
  }
}

Then, update your app.module.ts to import and use this controller:

import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
} from '@nestjs/common';
import { OsRedirectMiddleware } from './os-redirect.middleware';
import { WellKnownController } from './wellknown.controller';

@Module({
  imports: [],
  controllers: [WellKnownController],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(OsRedirectMiddleware)
      .exclude({ path: '.well-known/*', method: RequestMethod.ALL })
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

What We’ve Accomplished

Your apps are now connected to your server. When users click links from your domain:

  • If the app is installed: It opens directly to the specified content (deep link)
  • If the app is not installed: They’re redirected to the appropriate app store

Critical reminder: Your server must be deployed with HTTPS for Universal Links and App Links to work. These files must be accessible at:

[https://yourdomain.com/.well-known/apple-app-site-association](https://yourdomain.com/.well-known/apple-app-site-association)

[https://yourdomain.com/.well-known/assetlinks.json](https://yourdomain.com/.well-known/assetlinks.json)

Configure your app’s domain association

This final step tells your device to associate your app with the domain so when users tap your custom links, iOS and Android know to open your app instead of the browser.

Open your app.config.js (or app.json, if that’s what you use) in your React Native project, and update the platform configurations.

Android setup Inside your android object, add the following:

intentFilters: [
  {
    action: "VIEW",
    autoVerify: true,
    data: [
      {
        scheme: "https",
        host: "yourdomain.com",
        pathPrefix: "*",
      },
    ],
    category: ["BROWSABLE", "DEFAULT"],
  },
],

This enables Android App Links, automatically verifying that your app can handle any link from your domain.

iOS Setup Inside your ios object, add this:

associatedDomains: ["applinks:yourdomain.com"],

This enables Universal Links for iOS and ensures Apple devices know your domain is associated with your app.

Conclusion

You’ve just built your own Firebase Dynamic Links alternative from scratch.

Here’s what we accomplished:

  • Built a lightweight NestJS server that detects device type and redirects users to the correct store
  • Configured .well-known files to establish trust between your domain and app
  • Connected everything inside your React Native / Expo project so links open specific screens when the app is installed

But this setup can go even further :

  • You can serve rich metadata (like titles, thumbnails, and descriptions) so your links look great when shared in chats or on social media.
  • You can add link analytics to track opens, installs, and engagement.
  • Or even build a link shortener service using your own domain.

With just a small backend and a few configurations, you’ve recreated one of Firebase’s most useful features , fully under your control, no external dependency needed.

Further reading:


메타데이터
post_id
231a326e40de
slug
build-deep-links-for-react-native-firebase-alternative-231a326e40de
url
https://medium.com/@nnaemekaonyeji27/build-deep-links-for-react-native-firebase-alternative-231a326e40de
canonical_url
https://medium.com/@nnaemekaonyeji27/build-deep-links-for-react-native-firebase-alternative-231a326e40de
author_url
https://medium.com/@nnaemekaonyeji27
status
ok
fetched_at
2026-08-04 07:10:45