Push Notifications & Deep Linking Architecture

📖 Concept

Push notifications and deep linking are architecturally interconnected — a notification tap often deep links to a specific screen. Getting them right at scale requires careful architecture.

Push Notification Architecture:

Server → APNs (iOS) / FCM (Android) → Device OS → App
                                                    ↓
                                        Foreground: onMessage handler
                                        Background: onBackgroundMessage
                                        Killed: onNotificationOpenedApp

The 3 notification scenarios:

  1. Foreground — App is open and active. You receive the payload and decide how to display it (in-app banner, badge, ignore).
  2. Background — App is in memory but not visible. System shows the notification. When tapped, your app receives the data.
  3. Killed/Quit — App is not running. System shows the notification. When tapped, app launches and receives the initial notification.

Deep Linking Architecture: Deep links navigate users to specific content within your app. Sources:

  • Push notification taps
  • External URLs (email, SMS, web)
  • Universal Links (iOS) / App Links (Android)
  • Other apps (social media shares)

Architecture for handling deep links:

Deep Link Received
  → URL Parser (extract route + params)
  → Auth Gate (is user logged in?)
    → If no: queue link → show login → after login, navigate
    → If yes: navigate directly
  → Navigation Handler
    → Stack reset if needed (clear back stack)
    → Navigate to target screen with params

💻 Code Example

codeTap to expand ⛶
1// === PUSH NOTIFICATION ARCHITECTURE ===
2
3import messaging from '@react-native-firebase/messaging';
4import notifee, { AndroidImportance } from '@notifee/react-native';
5
6// 1. Notification Service — centralized handling
7class NotificationService {
8 private static instance: NotificationService;
9 private deepLinkHandler: DeepLinkHandler;
10
11 static getInstance() {
12 if (!this.instance) this.instance = new NotificationService();
13 return this.instance;
14 }
15
16 async initialize() {
17 // Request permission
18 const status = await messaging().requestPermission();
19 if (status === messaging.AuthorizationStatus.AUTHORIZED) {
20 const token = await messaging().getToken();
21 await this.registerToken(token);
22 }
23
24 // Token refresh
25 messaging().onTokenRefresh(this.registerToken);
26
27 // Foreground messages
28 messaging().onMessage(async (remoteMessage) => {
29 await this.handleForegroundNotification(remoteMessage);
30 });
31
32 // Background/quit notification tap
33 messaging().onNotificationOpenedApp((remoteMessage) => {
34 this.handleNotificationTap(remoteMessage);
35 });
36
37 // App opened from killed state via notification
38 const initialNotification = await messaging().getInitialNotification();
39 if (initialNotification) {
40 this.handleNotificationTap(initialNotification);
41 }
42 }
43
44 private async handleForegroundNotification(message: FirebaseMessagingTypes.RemoteMessage) {
45 // Show in-app notification using Notifee
46 await notifee.displayNotification({
47 title: message.notification?.title,
48 body: message.notification?.body,
49 data: message.data,
50 android: {
51 channelId: 'default',
52 importance: AndroidImportance.HIGH,
53 pressAction: { id: 'default' },
54 },
55 });
56 }
57
58 private handleNotificationTap(message: FirebaseMessagingTypes.RemoteMessage) {
59 // Extract deep link from notification data
60 const deepLink = message.data?.deepLink as string;
61 if (deepLink) {
62 this.deepLinkHandler.handle(deepLink);
63 }
64 }
65
66 private async registerToken(token: string) {
67 await api.registerPushToken({ token, platform: Platform.OS });
68 }
69}
70
71// === DEEP LINKING ARCHITECTURE ===
72
73// 2. Deep Link Handler with auth gating
74class DeepLinkHandler {
75 private pendingLink: string | null = null;
76 private navigationRef: NavigationContainerRef<any>;
77
78 constructor(navigationRef: NavigationContainerRef<any>) {
79 this.navigationRef = navigationRef;
80 }
81
82 async handle(url: string) {
83 const parsed = this.parseDeepLink(url);
84 if (!parsed) return;
85
86 // Auth gate
87 const isAuthenticated = await authService.isAuthenticated();
88 if (parsed.requiresAuth && !isAuthenticated) {
89 this.pendingLink = url; // Queue for after login
90 this.navigationRef.navigate('Login');
91 return;
92 }
93
94 // Navigate
95 this.navigateTo(parsed);
96 }
97
98 // Call this after successful login
99 processPendingLink() {
100 if (this.pendingLink) {
101 const link = this.pendingLink;
102 this.pendingLink = null;
103 this.handle(link);
104 }
105 }
106
107 private parseDeepLink(url: string): ParsedLink | null {
108 // myapp://product/123 → { screen: 'ProductDetail', params: { id: '123' } }
109 // myapp://chat/456 → { screen: 'ChatRoom', params: { roomId: '456' } }
110 const routes: Record<string, RouteConfig> = {
111 'product/:id': { screen: 'ProductDetail', requiresAuth: false },
112 'chat/:roomId': { screen: 'ChatRoom', requiresAuth: true },
113 'profile/:userId': { screen: 'UserProfile', requiresAuth: true },
114 'settings': { screen: 'Settings', requiresAuth: true },
115 };
116
117 // Match URL against route patterns and extract params
118 for (const [pattern, config] of Object.entries(routes)) {
119 const match = matchPath(url, pattern);
120 if (match) {
121 return { ...config, params: match.params };
122 }
123 }
124 return null;
125 }
126
127 private navigateTo(link: ParsedLink) {
128 // Reset stack and navigate — prevents deep back stacks
129 this.navigationRef.dispatch(
130 CommonActions.reset({
131 index: 1,
132 routes: [
133 { name: 'Home' }, // Always have Home at the bottom
134 { name: link.screen, params: link.params },
135 ],
136 })
137 );
138 }
139}
140
141// 3. React Navigation deep linking config
142const linking = {
143 prefixes: ['myapp://', 'https://myapp.com'],
144 config: {
145 screens: {
146 Home: '',
147 ProductDetail: 'product/:id',
148 ChatRoom: 'chat/:roomId',
149 UserProfile: 'profile/:userId',
150 Settings: 'settings',
151 },
152 },
153};

🏋️ Practice Exercise

Push Notifications & Deep Linking Exercises:

  1. Implement a complete push notification setup with Firebase Cloud Messaging
  2. Handle all 3 notification states (foreground, background, killed) correctly
  3. Build a deep link handler with auth gating and pending link queue
  4. Create notification channels on Android with different priorities
  5. Implement rich notifications with images and action buttons
  6. Test deep linking from: push notifications, external URLs, and universal links

⚠️ Common Mistakes

  • Not handling the 'app killed' notification scenario — getInitialNotification() is often forgotten, losing the user's intent

  • Navigating before the navigation container is ready — use a ref and wait for isReady signal

  • Not gating authenticated deep links — navigating to a chat room before login causes crashes or empty screens

  • Hardcoding deep link routes instead of using a route map — makes changes error-prone and untestable

  • Not handling notification permissions gracefully — crashing or showing nothing when permission is denied

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Push Notifications & Deep Linking Architecture. Login to unlock this feature.