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
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:
- Bundle Identifier: Found in your React Native project’s
app.config.jsunderios.bundleIdentifier. It looks likecom.companyname.appname - Team ID: Found in your Apple Developer account. This is a 10-character string (e.g.,
A1B2C3D4E5) - Final format:
A1B2C3D4E5.com.companyname.appname

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:
- Package Name: Found in your
app.config.jsunderandroid.package. It looks similar to your iOS bundle identifier. - EAS SHA-256 Certificate (first fingerprint):
- Navigate to your app on the Expo dashboard
- Go to Credentials → Android
- Under Build Credentials, find Android upload keystore
- Copy the SHA-256 Fingerprint

EAS SHA-256
3. Play Store SHA-256 Certificate (second fingerprint):
- Upload your build to Play Store (you can use internal testing)
- Navigate to Release → Setup → App Integrity
- Scroll to App signing , Click Settings.
- Copy the 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-knownroutes 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