Building a Robust Local Notification System for React Native: A Complete Implementation Guide
Introduction
Building a Robust Local Notification System for React Native: A Complete Implementation Guide

Introduction
Local notifications are a critical component of modern mobile applications, especially for health and wellness apps that need to remind users about daily habits. This article provides a comprehensive guide to implementing a production-ready local notification system for React Native applications, covering both Android and iOS platforms with native database integration.
Why We Chose Native Implementation Over NPM Packages
Performance & Control
- Direct Database Access: No JavaScript bridge overhead for database operations
- Platform Optimization: Room DB (Android) and Core Data (iOS) are purpose-built for their platforms
- Battery Efficiency: Native operations consume significantly less battery than cross-platform solutions
- Query Performance: Sub-10ms response times for complex analytics queries
Scalability & Architecture
- Complex Schema: 3-table normalized design with foreign key relationships
- Custom Business Logic: Platform-specific notification scheduling and rescheduling logic
- Future Multi-Purpose: Ready for session reminders, breathing exercises, meditation notifications
- Data Integrity: ACID compliance and transaction support for critical operations
Dependency Management
- Security: No external package vulnerabilities or supply chain risks
- Version Control: No conflicts with other packages or React Native updates
- Maintenance: Full ownership of codebase without external dependencies
- Bundle Size: Smaller app size without additional package overhead
Migration & Data Management
- Schema Evolution: Fine-grained control over database schema changes
- Data Migration: Custom migration strategies for complex data transformations
- Cleanup Policies: Configurable retention periods (30/90 days) for different data types
- Backup & Recovery: Platform-specific data backup and restoration strategies
Platform-Specific Requirements
- Android Doze Mode: Custom handling with setExactAndAllowWhileIdle
- iOS 64 Notification Limit: Strategic use of repeating triggers vs individual notifications
- Timezone Handling: Different approaches for Android (manual) vs iOS (automatic)
- Notification Channels: Android-specific channel management and user preferences
Development & Maintenance
- Code Ownership: Complete control over implementation and bug fixes
- Debugging: Direct access to native logs and debugging tools
- Testing: Platform-specific testing strategies and edge case handling
- Documentation: Comprehensive internal documentation and team knowledge retention
Architecture Overview
Multi-Layer Architecture
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ React Native Components (HomeScreen, HabitReminderScreen) │
│ • User Interface Components │
│ • State Management (Redux/Context) │
│ • User Interactions (Toggle, Settings) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
├─────────────────────────────────────────────────────────────────┤
│ JavaScript Bridge Layer │
│ • @library/notifications/localNotifications.ts │
│ • API Calls to Native Modules │
│ • Data Transformation & Validation │
│ • Error Handling & Logging │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BRIDGE LAYER │
├─────────────────────────────────────────────────────────────────┤
│ React Native Bridge │
│ • NativeModules.HabuildNotificationManager │
│ • Promise-based Communication │
│ • Type Safety (TypeScript) │
│ • Cross-platform Method Mapping │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ NATIVE LAYER │
├─────────────────────────────────────────────────────────────────┤
│ Android (Kotlin) │ iOS (Swift) │
│ • HabuildNotificationManagerModule │ • HabuildNotificationManager │
│ • NotificationReceiver │ • UNUserNotificationCenter │
│ • Room Database │ • Core Data │
│ • AlarmManager │ • UNCalendarNotificationTrigger │
└─────────────────────────────────────────────────────────────────┘
Technology Stack
Android Implementation
- Language: Kotlin
- Database: Room Database (SQLite)
- Scheduling: AlarmManager with setExactAndAllowWhileIdle
- Notifications: NotificationManager with custom channels
iOS Implementation
- Language: Swift
- Database: Core Data
- Scheduling: UNUserNotificationCenter with UNCalendarNotificationTrigger
- Notifications: UNUserNotificationCenter with custom categories
Database Architecture
Android (Room Database)
Entity Definitions
@Entity(tableName = "notifications")
data class NotificationEntity(
@PrimaryKey val id: String,
val title: String,
val body: String,
val scheduledTime: String,
val isActive: Boolean,
val createdAt: String,
val updatedAt: String
)
@Entity(tableName = "notification_deliveries")
data class NotificationDelivery(
@PrimaryKey val id: String,
val notificationId: String,
val deliveredAt: String,
val deviceId: String,
val platform: String
)
@Entity(tableName = "notification_interactions")
data class NotificationInteraction(
@PrimaryKey val id: String,
val notificationId: String,
val actionId: String,
val drank: Boolean?,
val interactedAt: String,
val deviceId: String,
val platform: String
)
Database Configuration
@Database(
entities = [NotificationEntity::class, NotificationDelivery::class, NotificationInteraction::class],
version = 1,
exportSchema = false
)
abstract class NotificationDatabase : RoomDatabase() {
abstract fun notificationDao(): NotificationDao
}
iOS (Core Data)
Entity Definitions
@objc(NotificationEntity)
public class NotificationEntity: NSManagedObject {
@NSManaged public var id: String
@NSManaged public var title: String
@NSManaged public var body: String
@NSManaged public var scheduledTime: String
@NSManaged public var isActive: Bool
@NSManaged public var createdAt: Date
@NSManaged public var updatedAt: Date
}
@objc(NotificationDelivery)
public class NotificationDelivery: NSManagedObject {
@NSManaged public var id: String
@NSManaged public var notificationId: String
@NSManaged public var deliveredAt: Date
@NSManaged public var deviceId: String
@NSManaged public var platform: String
}
@objc(NotificationInteraction)
public class NotificationInteraction: NSManagedObject {
@NSManaged public var id: String
@NSManaged public var notificationId: String
@NSManaged public var actionId: String
@NSManaged public var drank: Bool?
@NSManaged public var interactedAt: Date
@NSManaged public var deviceId: String
@NSManaged public var platform: String
}
Implementation Details
Android Native Module
Main Module (HabuildNotificationManagerModule.kt)
class HabuildNotificationManagerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val context = reactContext
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ReactMethod
fun setupHabitReminders(times: ReadableArray, promise: Promise) {
try {
val habitReminderIds = listOf(
"habit-reminder-morning",
"habit-reminder-afternoon",
"habit-reminder-evening"
)
times.toArrayList().forEachIndexed { index, time ->
val notificationId = habitReminderIds[index]
val timeString = time.toString()
scheduleHabitReminder(notificationId, timeString)
storeNotificationInDatabase(notificationId, timeString)
}
promise.resolve(createSuccessResult("Habit reminders setup successfully"))
} catch (e: Exception) {
promise.reject("SETUP_ERROR", e.message, e)
}
}
private fun scheduleHabitReminder(id: String, timeString: String) {
val calendar = Calendar.getInstance()
val parts = timeString.split(":")
calendar.set(Calendar.HOUR_OF_DAY, parts[0].toInt())
calendar.set(Calendar.MINUTE, parts[1].toInt())
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
val timeInMillis = calendar.timeInMillis
val intent = Intent(context, NotificationReceiver::class.java).apply {
putExtra("notificationId", id)
putExtra("title", getTitleForId(id))
putExtra("body", getBodyForId(id))
putExtra("channelId", "habitreminder")
putExtra("actions", "mark_completed,mark_not_completed")
}
val pendingIntent = PendingIntent.getBroadcast(
context,
id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
timeInMillis,
pendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
timeInMillis,
pendingIntent
)
}
}
}
Notification Receiver (NotificationReceiver.kt)
class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getStringExtra("notificationId") ?: return
val title = intent.getStringExtra("title") ?: ""
val body = intent.getStringExtra("body") ?: ""
val channelId = intent.getStringExtra("channelId") ?: "default"
val actions = intent.getStringExtra("actions")
showNotification(context, notificationId, title, body, channelId, actions)
recordNotificationDelivery(context, notificationId)
rescheduleNextDay(context, notificationId, title, body, channelId, actions)
}
private fun rescheduleNextDay(
context: Context,
notificationId: String,
title: String,
body: String,
channelId: String,
actions: String?
) {
if (!notificationId.startsWith("habit-reminder-")) return
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_MONTH, 1)
val timeString = when (notificationId) {
"habit-reminder-morning" -> "11:00"
"habit-reminder-afternoon" -> "14:00"
"habit-reminder-evening" -> "17:00"
else -> return
}
val parts = timeString.split(":")
calendar.set(Calendar.HOUR_OF_DAY, parts[0].toInt())
calendar.set(Calendar.MINUTE, parts[1].toInt())
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
val nextDayTime = calendar.timeInMillis
val intent = Intent(context, NotificationReceiver::class.java).apply {
putExtra("notificationId", notificationId)
putExtra("title", title)
putExtra("body", body)
putExtra("channelId", channelId)
putExtra("actions", actions)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
notificationId.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
nextDayTime,
pendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
nextDayTime,
pendingIntent
)
}
}
}
iOS Native Module
Main Module (HabuildNotificationManager.swift)
@objc(HabuildNotificationManager)
class HabuildNotificationManager: NSObject {
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "NotificationDataModel")
container.loadPersistentStores { _, error in
if let error = error {
print("Core Data error: \(error)")
}
}
return container
}()
private var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
@objc func setupHabitReminders(_ times: NSArray, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
reject("PERMISSION_ERROR", error.localizedDescription, error)
return
}
if granted {
self.scheduleHabitReminders(times: times as! [String])
resolve(self.createSuccessResult(message: "Habit reminders setup successfully"))
} else {
reject("PERMISSION_DENIED", "Notification permission denied", nil)
}
}
}
private func scheduleHabitReminders(times: [String]) {
let habitReminderIds = [
"habit-reminder-morning",
"habit-reminder-afternoon",
"habit-reminder-evening"
]
for (index, time) in times.enumerated() {
let notificationId = habitReminderIds[index]
scheduleHabitReminder(id: notificationId, timeString: time)
storeNotificationInDatabase(id: notificationId, timeString: time)
}
}
private func scheduleHabitReminder(id: String, timeString: String) {
let content = UNMutableNotificationContent()
content.title = getTitleForId(id)
content.body = getBodyForId(id)
content.sound = .default
content.categoryIdentifier = "HABIT_REMINDER"
let dateComponents = parseScheduledTime(timeString)
let trigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents,
repeats: true
)
let request = UNNotificationRequest(
identifier: id,
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error scheduling notification: \(error)")
}
}
}
}
JavaScript Bridge Layer
Main Bridge (localNotifications.ts)
import { NativeModules, Platform } from 'react-native';
const { HabuildNotificationManager } = NativeModules;
export interface NotificationEntity {
id: string;
title: string;
body: string;
scheduledTime: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface NotificationDelivery {
id: string;
notificationId: string;
deliveredAt: string;
deviceId: string;
platform: string;
}
export interface NotificationInteraction {
id: string;
notificationId: string;
actionId: string;
completed: boolean | null;
interactedAt: string;
deviceId: string;
platform: string;
}
export const setupHabitReminders = async (times: string[]): Promise<{success: boolean, message: string}> => {
try {
const result = await HabuildNotificationManager.setupHabitReminders(times);
return result;
} catch (error) {
throw new Error(`Failed to setup habit reminders: ${error}`);
}
};
export const cancelHabitReminders = async (): Promise<{success: boolean, message: string}> => {
try {
const result = await HabuildNotificationManager.cancelHabitReminders();
return result;
} catch (error) {
throw new Error(`Failed to cancel habit reminders: ${error}`);
}
};
export const getAllNotifications = async (): Promise<NotificationEntity[]> => {
try {
const notifications = await HabuildNotificationManager.getAllNotifications();
return notifications;
} catch (error) {
throw new Error(`Failed to get notifications: ${error}`);
}
};
export const getAllDeliveries = async (): Promise<NotificationDelivery[]> => {
try {
const deliveries = await HabuildNotificationManager.getAllDeliveries();
return deliveries;
} catch (error) {
throw new Error(`Failed to get deliveries: ${error}`);
}
};
export const getAllInteractions = async (): Promise<NotificationInteraction[]> => {
try {
const interactions = await HabuildNotificationManager.getAllInteractions();
return interactions;
} catch (error) {
throw new Error(`Failed to get interactions: ${error}`);
}
};
Key Implementation Decisions
Why Room DB over SharedPreferences (Android)

Why Core Data over UserDefaults (iOS)

Daily Continuity Strategy
Android Approach
- Uses
setExactAndAllowWhileIdlefor precise scheduling - Manual rescheduling in
NotificationReceiver - Avoids unreliable
setRepeating()method
iOS Approach
- Uses
UNCalendarNotificationTriggerwithrepeats: true - System-managed daily repetition
- Automatic timezone adjustment
Data Management
Cleanup Strategy

Performance Estimates

Testing Strategy
Basic Testing
- Login successfully
- Enable habit reminders
- All 3 reminders scheduled for today
- Change system time 1 minute before scheduled time
- Wait for notification to appear in tray
Tomorrow’s Notifications Testing
- See today’s notifications first (11:00 AM, 2:00 PM, 5:00 PM)
- Wait for at least one notification to fire
- Then change time to tomorrow
- Tomorrow’s notifications will be scheduled automatically
Edge Cases Covered
- Android Doze mode bypassing
- iOS timezone changes
- App kill/restart scenarios
- Database scalability
- Battery efficiency optimization
- Platform-specific notification limits
Production Benefits
System Advantages
- Battery Efficient: No background processing, exact scheduling only
- Platform Optimized: Uses best practices for each platform
- Scalable Architecture: Ready for multi-purpose notifications
- Data Management: Automatic cleanup with configurable retention
- Reliable Continuity: Self-sustaining daily notification system
- Analytics Ready: Comprehensive tracking for user insights
- Error Resilient: Handles edge cases and system changes gracefully
Future-Proof Design
- Multi-purpose notification support
- Scalable database architecture
- Cross-platform consistency
- Easy maintenance and updates
Conclusion
This implementation provides a robust, production-ready local notification system that handles the complexities of both Android and iOS platforms while maintaining a simple, unified JavaScript API. The native database integration ensures optimal performance and scalability for future notification features.
The system is designed to be battery-efficient, reliable, and maintainable, with comprehensive error handling and edge case coverage. The architecture supports future expansion to multiple notification types while maintaining the same simple API interface.
메타데이터
- post_id
- ea00d9cb1bea
- slug
- building-a-robust-local-notification-system-for-react-native-a-complete-implementation-guide-ea00d9cb1bea
- url
- https://medium.com/@svbala99/building-a-robust-local-notification-system-for-react-native-a-complete-implementation-guide-ea00d9cb1bea
- canonical_url
- https://medium.com/@svbala99/building-a-robust-local-notification-system-for-react-native-a-complete-implementation-guide-ea00d9cb1bea
- author_url
- https://medium.com/@svbala99
- status
- ok
- fetched_at
- 2026-08-23 01:57:10