smaple.tr
offline-first mobile app

Offline-First Mobile App: Local-First Architecture, Sync Strategies, and Push Notification Infrastructure [2026]

Mehmet Kurtipek
March 25, 2026
12 min read
offline-first mobile app
local-first architecture
CRDT conflict resolution
push notifications
background sync

The assumption that users always have a reliable network connection is a design flaw. According to OpenSignal's connectivity report, mobile users experience no connectivity for an average of 8% of their mobile usage time — and that percentage climbs dramatically in logistics, field service, healthcare, and construction environments where offline-first applications have the most value.

This guide covers the complete offline-first mobile architecture stack: local database selection, conflict resolution strategies (including CRDTs), background synchronization patterns for iOS and Android, and push notification infrastructure. By the end, you will have a concrete architecture decision framework for building mobile applications that work as well at 2G as at gigabit WiFi.

Offline-First Mobile App: The Design Philosophy

Offline-first is not the same as offline-capable. An offline-capable app degrades gracefully when connectivity is lost. An offline-first app treats local operation as the primary mode and synchronization with the server as a secondary, background process.

Three principles distinguish offline-first architecture:

Local-first reads. Every read operation is served from local storage. The app never blocks a user interaction waiting for a network response. When connectivity returns, the local data is synchronized with the server — but the user's experience never depends on that synchronization completing in real time.

Optimistic writes. User actions are applied immediately to local state. The UI reflects the change instantly. Server synchronization happens asynchronously in the background. If synchronization fails, the user is notified — but the work they did is preserved locally and retried.

Deterministic conflict resolution. When the same data is modified on multiple devices during a disconnection window, the merge strategy is explicit and consistent. Users are never surprised by silent data loss.

This architecture is appropriate for: field service apps, logistics and delivery, healthcare record entry, sales force automation, note-taking and document editing, collaborative task management.

Local Database Selection

SQLite

SQLite is the embedded database available natively on both iOS and Android without additional dependencies. For iOS and Android native apps, SQLite provides full SQL query capability, ACID transaction guarantees, and performance adequate for datasets up to several hundred MB.

Framework-level SQLite access:

  • Android: Room Persistence Library (type-safe Kotlin API, migration support, coroutine integration)
  • iOS: Core Data with SQLite backing, or direct GRDB access for teams preferring SQL
  • Flutter: Drift (formerly Moor) — type-safe SQLite ORM with reactive queries and migration DSL
  • React Native: expo-sqlite, or react-native-sqlite-storage for bare workflow
// Flutter Drift - reactive query with sync status tracking
@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
  Stream<List<Task>> watchPendingTasks() =>
    (select(tasks)..where((t) => t.syncStatus.equals(1))).watch();
}

The syncStatus column pattern (0 = synced, 1 = pending upload, 2 = conflict) is the minimal state machine for tracking which records need synchronization.

Realm

Realm is an object-oriented embedded database built specifically for mobile. Its reactive query model (results update automatically when underlying data changes) reduces the UI synchronization code needed in applications with frequently changing data.

Realm's integration with MongoDB Atlas Device Sync provides automatic, managed synchronization with a cloud backend — useful when teams want to avoid building a custom sync engine. The tradeoff: the application is coupled to the MongoDB Atlas platform and Realm's partition-based or flexible sync models.

When to choose Realm over SQLite: Applications with complex object graphs, frequent real-time updates, and teams who want managed sync rather than a custom sync engine.

WatermelonDB

WatermelonDB is a React Native-specific local database built for high performance with large record counts. Its lazy loading and observable query patterns keep UI performance consistent when working with datasets that would slow down naive SQLite implementations.

// WatermelonDB model with sync status
class Task extends Model {
  static table = 'tasks';

  @text('title') title;
  @field('completed') completed;
  @field('sync_status') syncStatus; // 0: synced, 1: pending, 2: conflict
  @date('updated_at') updatedAt;
  @relation('projects', 'project_id') project;
}

Conflict Resolution Strategies

Conflicts are unavoidable in any system where the same data can be modified on multiple devices simultaneously. The conflict resolution strategy must be chosen before development begins — retrofitting it is expensive.

Last-Write-Wins (LWW)

The simplest strategy: each record carries a timestamp, and the record with the later timestamp wins when two versions are merged.

interface VersionedRecord {
  id: string;
  data: unknown;
  updatedAt: number; // Unix milliseconds
  deviceId: string;  // for deterministic tie-breaking
}

function resolveConflict(
  local: VersionedRecord,
  remote: VersionedRecord
): VersionedRecord {
  if (local.updatedAt > remote.updatedAt) return local;
  if (remote.updatedAt > local.updatedAt) return remote;
  // Identical timestamps: use deviceId for deterministic tie-breaking
  return local.deviceId > remote.deviceId ? local : remote;
}

LWW is appropriate when: data is not collaborative (each user owns their records), losing one version of a change is acceptable, and simplicity of implementation outweighs data preservation.

Risk: If two users edit the same record within the timestamp resolution (millisecond clocks on mobile can drift), one change is silently lost.

Field-Level Merge

Instead of resolving conflicts at the record level, resolve them at the field level: if two versions of the same record modified different fields, both changes are preserved.

function mergeFields(
  base: Record<string, unknown>,
  local: Record<string, unknown>,
  remote: Record<string, unknown>,
  localTimestamps: Record<string, number>,
  remoteTimestamps: Record<string, number>
): Record<string, unknown> {
  const merged = { ...base };
  for (const key of Object.keys({ ...local, ...remote })) {
    const localTs = localTimestamps[key] ?? 0;
    const remoteTs = remoteTimestamps[key] ?? 0;
    merged[key] = localTs >= remoteTs ? local[key] : remote[key];
  }
  return merged;
}

Field-level merge significantly reduces data loss in collaborative editing scenarios without the full complexity of CRDTs.

CRDT (Conflict-free Replicated Data Types)

CRDTs are data structures with mathematically guaranteed conflict-free merges — any two replicas can be merged in any order and produce identical results. This property makes them ideal for collaborative real-time editing without a central coordinator.

Common CRDT types:

Type Use Case Behavior
G-Counter Like count, view count Monotonically increasing; merge takes max per node
LWW-Register Single field value Per-field timestamp; last write wins per field
OR-Set Tag lists, participant lists Add-wins on concurrent add+remove
RGA Ordered lists, text editing Preserves insertion order across replicas
// LWW-Register implementation
interface LWWRegister<T> {
  value: T;
  timestamp: number;
  nodeId: string; // Unique per device
}

function merge<T>(
  a: LWWRegister<T>,
  b: LWWRegister<T>
): LWWRegister<T> {
  if (a.timestamp > b.timestamp) return a;
  if (b.timestamp > a.timestamp) return b;
  return a.nodeId > b.nodeId ? a : b; // Deterministic on tie
}

CRDT libraries for mobile: Automerge (JavaScript/Rust, React Native compatible), Yjs (JavaScript, WASM build for React Native), and custom implementations for simpler types (counters, sets).

When to use CRDTs: Collaborative editing, multi-device document sync, shared task lists where two users may simultaneously add or remove items.

Background Synchronization

Android WorkManager

WorkManager is the current recommended approach for background work on Android. It handles OS battery optimization (Doze mode, app standby), survives app restarts, and supports constraint-based scheduling.

// Periodic sync every 15 minutes when network is available
val syncRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
    repeatInterval = 15,
    repeatIntervalTimeUnit = TimeUnit.MINUTES
).setConstraints(
    Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build()
).build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "data_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    syncRequest
)

Key WorkManager behaviors to understand: periodic work has a minimum interval of 15 minutes enforced by the OS; one-time work can be triggered immediately; constraints (network, battery, storage) must all be satisfied before the work runs.

iOS BGTaskScheduler

iOS 13+ provides BGTaskScheduler for background refresh and processing tasks. Background execution time is allocated by the OS based on usage patterns — apps that are used frequently receive more background time.

// Register background task
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.smartmaple.datasync",
    using: .main
) { task in
    self.performDataSync(task: task as! BGAppRefreshTask)
}

// Schedule the next refresh
func scheduleBackgroundSync() {
    let request = BGAppRefreshTaskRequest(
        identifier: "com.smartmaple.datasync"
    )
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    try? BGTaskScheduler.shared.submit(request)
}

iOS background execution is more constrained than Android. BGAppRefreshTask allows approximately 30 seconds of background CPU time. BGProcessingTask allows longer operations but requires the device to be charging and idle. Plan sync operations to be incremental (delta sync) rather than full-corpus.

Delta Sync Architecture

Full sync (download all records every sync cycle) is inefficient and unnecessary. Delta sync downloads only records changed since the last successful sync.

// Delta sync — server returns only changes since lastSyncAt
async function deltaSync(db, apiClient) {
  const lastSyncAt = await db.getLastSyncTimestamp();

  // Upload local pending changes
  const pending = await db.getPendingChanges();
  if (pending.length > 0) {
    const uploadResult = await apiClient.uploadChanges(pending);
    await db.markAsSynced(uploadResult.acceptedIds);
  }

  // Download server changes since last sync
  const { changes, serverTimestamp } = await apiClient.getChanges({
    since: lastSyncAt
  });

  await db.applyServerChanges(changes);
  await db.setLastSyncTimestamp(serverTimestamp);
}

The server's since timestamp should be the server's clock time (not the client's) to prevent missed updates from clock drift between devices.

Push Notification Infrastructure

Push notifications re-engage users after they have left the app. When implemented correctly — targeted, timely, and actionable — they increase 90-day retention by up to 88% (Localytics). When implemented incorrectly (excessive frequency, generic content), they drive uninstalls.

APNs and FCM Architecture

Push delivery requires two platform-specific services:

  • APNs (Apple Push Notification service): Delivers notifications to iOS, macOS, watchOS, and tvOS devices
  • FCM (Firebase Cloud Messaging): Delivers to Android devices (also wraps APNs for cross-platform SDKs)

Both services require:

  1. Device registers with APNs/FCM → receives a device token
  2. App sends device token to your backend and stores it associated with the user
  3. Backend calls APNs/FCM API with payload and device token
  4. Platform service delivers notification to device
// Backend notification dispatch (Node.js with firebase-admin)
const admin = require('firebase-admin');

async function sendNotification(deviceToken, payload) {
  const message = {
    token: deviceToken,
    notification: {
      title: payload.title,
      body: payload.body,
    },
    data: {
      type: payload.type,
      referenceId: String(payload.referenceId),
    },
    android: {
      priority: 'high',
      notification: { channelId: 'important_updates' },
    },
    apns: {
      payload: {
        aps: {
          sound: 'default',
          badge: payload.badgeCount,
        },
      },
    },
  };
  return admin.messaging().send(message);
}

Rich Notifications

Rich notifications include images, video, or custom UI beyond the standard title/body. They significantly improve engagement rates for e-commerce and media applications.

iOS (Notification Service Extension): A separate process that intercepts the notification before display, downloads attached media, and modifies the content. Requires mutable-content: 1 in the APNs payload.

Android (BigPictureStyle): Includes a large image in the notification expansion. Configured through NotificationCompat.BigPictureStyle. No server-side extension required — the image URL is in the notification payload.

Notification Segmentation

Bulk notifications sent to the entire user base produce low engagement and high opt-out rates. Segmentation targets users based on behavior, preference, or transaction state.

Segment Trigger Notification Content
Cart abandonment Item in cart, no purchase for 2 hours "Your [item] is still waiting"
Re-engagement No app open for 7 days Personalized content highlight
Transactional Order status change "Your order has shipped"
Preference-based User opted in to category New items in followed category

Optimal notification timing maximizes delivery-to-open conversion: scheduling based on when the individual user is most likely to engage (learned from historical open timestamps) outperforms static time-of-day scheduling by 30–40% for consumer apps.

Sync State UI

Users need to know when their data is current and when it is pending synchronization. Sync state indicators should be informative without being distracting.

State machine:

State Description UI Pattern
SYNCED All local changes synchronized Green dot or no indicator (default)
PENDING Local changes awaiting upload Subtle badge or status bar indicator
SYNCING Active synchronization in progress Animated spinner in status area
CONFLICT Manual resolution required Red indicator + in-app notification
ERROR Sync failed; will retry Error toast with retry action

The conflict state should surface specific conflicting records to the user with clear choices: "Keep your version," "Use server version," or "View both and decide." Hiding conflicts silently produces data inconsistencies that are discovered weeks later and are nearly impossible to audit.

Testing Offline-First Applications

Testing offline-first behavior requires deliberately controlled connectivity scenarios:

iOS Simulator: Use Charles Proxy or mitmproxy to simulate network conditions. iOS Network Link Conditioner allows simulating specific bandwidth and latency profiles.

Android Emulator: Android Studio's extended controls include network speed simulation. ADB commands can disable network: adb shell svc wifi disable.

Test scenarios that must pass:

  1. Create a record offline → reconnect → verify record appears on server
  2. Edit a record on device A offline → edit same record on device B offline → reconnect both → verify conflict resolution produces expected result
  3. 1,000 pending records → reconnect → verify sync completes without errors or data loss
  4. Background sync during device lock → verify sync ran in next analytics report

Conclusion

Offline-first mobile architecture requires upfront design investment that pays back in user retention, especially in environments with unreliable connectivity. The core decisions — local database choice, conflict resolution strategy, sync architecture — must be made before the first sprint, not discovered during development.

The sync implementation is the most complex part: delta sync keeps bandwidth and sync time manageable, background sync handles the iOS and Android scheduler constraints, and explicit conflict resolution prevents the silent data loss that undermines user trust in an offline-first application.

Push notifications complete the engagement loop — bringing users back to the application with timely, relevant content. The infrastructure requirement (APNs/FCM integration, device token management, segmentation) is substantial but well-supported by the Firebase Admin SDK and modern BaaS platforms.

Related Articles

August 11, 2026

MLOps Guide: Taking Machine Learning Models to Production [2026]

87% of machine learning models built by data science teams never reach production. The models work — they pass cross-validation, they score well on holdout sets, they demonstrate genuine predictive value. The problem is not the modeling. The problem is everything that happens between a notebook experiment and a reliable, monitored, production system. MLOps is the discipline that closes that gap. This guide covers the full MLOps stack: maturity levels, tooling choices (MLflow, DVC, Kubeflow

Read More
August 10, 2026

LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]

General-purpose LLMs are impressive. They can write code, summarize documents, answer questions, and translate between languages with reasonable accuracy. But "reasonable" is not good enough when your application requires consistent output format, domain-specific terminology, a particular tone, or behavior that the base model was never trained to exhibit. That gap is where fine-tuning matters. Fine-tuning updates a model's weights on your specific data, changing how the model behaves — not

Read More
August 9, 2026

Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]

Computer vision has moved well past the research phase. The models are trained, the frameworks are mature, the hardware is accessible, and the use cases are generating measurable returns. What was a specialized capability requiring deep expertise in 2018 is now deployable infrastructure — if you know which component to reach for and where the real complexity lives. This guide covers computer vision applications across industrial, medical, logistics, and document processing domains. It expl

Read More