Skip to main content

Architecture Overview

This document describes the high-level architecture of @gabriel-sisjr/react-native-background-location. The library follows a three-layer design that separates the public TypeScript API from platform-specific native implementations through a TurboModule bridge contract.

Three-Layer Design

graph TD
A["TypeScript API Layer<br/>(src/index.tsx, hooks, types)"] -->|enum-to-string conversion| B["TurboModule Spec Layer<br/>(NativeBackgroundLocation.ts)"]
B -->|Codegen| C1["Android Native Layer<br/>(Kotlin, Room, Coroutines)"]
B -->|Codegen| C2["iOS Native Layer<br/>(Swift, Core Data, CLLocationManager)"]

style A fill:#3178c6,color:#fff
style B fill:#f0db4f,color:#333
style C1 fill:#a4c639,color:#fff
style C2 fill:#147efb,color:#fff

Layer 1: TypeScript API

The public-facing layer that application code consumes directly. It provides:

  • Functions -- startTracking, stopTracking, isTracking, getLocations, clearTrip, updateNotification, and the full geofencing API.
  • React hooks -- useLocationPermissions, useBackgroundLocation, useLocationTracking, useLocationUpdates, useGeofencing, useGeofenceEvents.
  • Types and enums -- LocationAccuracy, NotificationPriority, TrackingOptions, Coords, GeofenceRegion, and others.
  • Error classes -- GeofenceError with typed GeofenceErrorCode discriminator.

This layer handles enum-to-string conversion, JSON serialization of complex objects, input validation, and graceful degradation when the native module is unavailable (e.g., simulators).

The library publishes named exports only. There is no default export. Import individual functions directly: import { startTracking, stopTracking } from '@gabriel-sisjr/react-native-background-location'.

Layer 2: TurboModule Spec

NativeBackgroundLocation.ts defines the Spec extends TurboModule interface consumed by React Native Codegen. This is the contract between TypeScript and native code. Key constraints imposed by Codegen shape this layer:

  • No TypeScript enums -- Enum values are passed as plain strings. The TypeScript layer converts enums before crossing the bridge.
  • No complex object arrays -- Geofence regions and notification actions are JSON-serialized into strings.
  • Inline interface definitions -- TrackingOptionsSpec and PermissionStatusResult are defined directly in the spec file for Codegen compatibility.

Layer 3: Native Implementations

Each platform implements the TurboModule spec using platform-idiomatic patterns:

ConcernAndroidiOS
LanguageKotlinSwift + Objective-C++ bridge
Background executionForeground Service (LocationService)CLLocationManager background mode
PersistenceRoom Database (SQLite)Core Data (SQLite)
Event streamingSharedFlow singletons + coroutine collectionRCTEventEmitter
Crash recoveryWorkManager (RecoveryWorker)Significant location monitoring (RecoveryManager)
Location providerDual: FusedLocationProvider / AndroidLocationProviderCLLocationManager
GeofencingGeofencingClient (Google Play Services)CLLocationManager region monitoring

Key Design Decisions

TurboModule Over Bridge Modules

The library uses React Native's New Architecture (TurboModules) exclusively. TurboModules provide synchronous native module access, type-safe Codegen contracts, and lazy initialization -- all critical for a library that manages long-running background services.

Enum-to-String Conversion at the Bridge

Codegen does not support TypeScript enums. Rather than exposing raw strings in the public API, the library maintains proper TypeScript enums (LocationAccuracy, NotificationPriority) and converts them to strings in src/index.tsx before they cross the bridge via toTrackingOptionsSpec(). This keeps the public API type-safe while satisfying Codegen constraints.

JSON Serialization for Complex Objects

Codegen does not support typed object arrays. Geofence regions, notification actions, and notification options are serialized to JSON strings in TypeScript and deserialized on the native side. This pattern keeps the spec interface simple while supporting arbitrarily complex data structures.

Named Exports Only

The library exports individual named functions and hooks rather than a monolithic class or default export. This enables tree-shaking, improves discoverability in IDE autocompletion, and avoids the fragile singleton pattern.

Graceful Degradation

Every public function checks for native module availability before calling into native code. When the module is unavailable (simulators, misconfigured builds), functions log a warning and return sensible fallback values instead of crashing. Geofencing functions throw explicitly since silent degradation would be misleading for geofence registration.

Stop Token Mechanism

A SharedPreferences/UserDefaults flag with a 60-second TTL prevents the recovery system from restarting tracking after an explicit stopTracking() call. The token is checked at multiple points in the recovery pipeline to eliminate race conditions between stop and recovery.

Restart Loop Detection (Android)

A counter in SharedPreferences tracks service restarts within a 1-hour window. If the count exceeds 5, the service refuses to start and clears tracking state. This prevents infinite crash-restart loops from draining battery or producing system ANRs.

Batched Database Writes

Both platforms buffer location writes and flush in batches (up to 10 items, or every 5 seconds). This reduces I/O overhead during high-frequency GPS updates. A forceFlush() call guarantees consistency before any read operation.

Technology Stack

ComponentTechnologyRationale
JS/TS layerTypeScript, ReactType safety, hook-based API
BridgeTurboModule + CodegenType-safe contract, lazy init, New Architecture
Android moduleKotlinCoroutines, null safety, Android-first language
Android persistenceRoom (SQLite)Type-safe queries, reactive flows, schema versioning
Android concurrencyCoroutines (SupervisorJob + Dispatchers.Main)Structured concurrency, cancellation support
Android backgroundForeground Service + WorkManagerReliable background execution, OS compliance
Android locationGoogle Play Services (FusedLocationProvider)Battery-efficient, fused sensor data
Android fallbackAndroid LocationManagerDevices without Google Play Services
iOS moduleSwift + Objective-C++ bridgeModern Swift with TurboModule bridge compatibility
iOS persistenceCore Data (SQLite)Apple-native, thread-safe, schema migration
iOS locationCLLocationManagerOnly official iOS location API
iOS recoverySignificant location monitoringSystem-managed, low-power app relaunch
Build systemreact-native-builder-bobCommonJS + ESM + TypeScript declarations
MonorepoYarn 3.6.1 + TurborepoWorkspace management, parallel builds

Cross-Cutting Concerns

Event System

Both platforms emit the same four event types to JavaScript:

EventDescription
onLocationUpdateNew location coordinate received
onLocationErrorFatal error (permission revoked, provider error)
onLocationWarningNon-fatal warning (service timeout, task removed, GPS lost)
onNotificationActionNotification action button pressed

The event names and payload shapes are identical on both platforms, providing a unified API surface for hooks like useLocationUpdates and useGeofenceEvents.

Permission Model

The library provides a cross-platform permission flow through useLocationPermissions:

  • Android: Requests ACCESS_FINE_LOCATION, then ACCESS_BACKGROUND_LOCATION (API 29+), then POST_NOTIFICATIONS (API 33+).
  • iOS: Requests "When In Use" first, then escalates to "Always" via a two-step flow with a shouldIgnoreNextAuthCallback guard to handle iOS delegate timing.

Persistence and Recovery

Both platforms persist tracking state (active status, trip ID, options) to the database. On crash or system kill, the recovery mechanism reads this state and resumes tracking automatically. The stop token prevents recovery after an intentional stop.

Next Steps