Skip to main content
โšก Calmops

Building Offline-First Mobile Apps: Strategies and Implementation

Introduction

Offline-first apps work without internet and sync when connected. This guide covers strategies and implementation for building offline-capable mobile apps.


Offline-First Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Offline-First Data Flow                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚   UI Layer    โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ Local Storage โ”‚             โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚           โ”‚                           โ”‚                     โ”‚
โ”‚           โ”‚   Online?                โ”‚                     โ”‚
โ”‚           โ–ผ                          โ–ผ                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚  API Client   โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚  Sync Engine  โ”‚             โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                    โ”‚                      โ”‚
โ”‚                                    โ–ผ                      โ”‚
โ”‚                             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚                             โ”‚  Remote API   โ”‚             โ”‚
โ”‚                             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Local Storage Options

// 1. AsyncStorage - Simple key-value
import AsyncStorage from '@react-native-async-storage/async-storage';

// Save
await AsyncStorage.setItem('user', JSON.stringify(user));
await AsyncStorage.setItem('settings', JSON.stringify(settings));

// Load
const user = JSON.parse(await AsyncStorage.getItem('user'));

// 2. SQLite - Full database
// npm install expo-sqlite
import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('mydb');

// Create table
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    name TEXT,
    email TEXT,
    synced INTEGER DEFAULT 0
  );
`);

// 3. WatermelonDB - Reactive database
// Best for complex offline apps

Sync Strategies

Optimistic Updates

// Save locally first, then sync
async function createPost(content: string) {
  const post = {
    id: generateId(),
    content,
    createdAt: new Date().toISOString(),
    synced: false,
  };
  
  // 1. Save to local DB immediately
  await db.posts.create(post);
  
  // 2. Update UI (optimistic)
  addPostToList(post);
  
  // 3. Try to sync in background
  try {
    await api.createPost(post);
    await db.posts.update(post.id, { synced: true });
  } catch (error) {
    // Will retry later
    console.log('Sync failed, will retry');
  }
}

Background Sync

// Sync service
import * as BackgroundFetch from 'expo-background-fetch';

async function syncPendingPosts() {
  const pendingPosts = await db.posts
    .where('synced', false)
    .fetch();
  
  for (const post of pendingPosts) {
    try {
      await api.createPost(post);
      await db.posts.update(post.id, { synced: true });
    } catch (error) {
      // Continue with next
    }
  }
}

// Register background task
BackgroundFetch.registerTaskAsync('sync', {
  minimumInterval: 15 * 60, // 15 minutes
}, syncPendingPosts);

Conflict Resolution

Strategies

conflict_resolution:
  last_write_wins:
    - "Simple: most recent wins"
    - "Good for non-critical data"
    
  server_wins:
    - "Server always correct"
    - "Client changes overwritten"
    
  client_wins:
    - "Client always correct"
    - "For user-generated content"
    
  merge:
    - "Combine changes intelligently"
    - "Complex but powerful"

Implementation

async function syncWithConflictResolution(localPost, serverPost) {
  // Compare timestamps
  const localTime = new Date(localPost.updatedAt);
  const serverTime = new Date(serverPost.updatedAt);
  
  if (localTime > serverTime) {
    // Local is newer - push to server
    await api.updatePost(localPost.id, localPost);
  } else {
    // Server is newer - update local
    await db.posts.update(localPost.id, serverPost);
  }
}

Key Takeaways

  • Local-first - Save locally, sync when online
  • AsyncStorage - Simple key-value storage
  • SQLite - Full database capabilities
  • Optimistic UI - Update immediately, sync later

External Resources

Comments