home / skills / secondsky / claude-skills / mobile-offline-support

This skill helps you build offline-first mobile apps with robust data syncing, conflict handling, and queue management for resilient connectivity.

npx playbooks add skill secondsky/claude-skills --skill mobile-offline-support

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
4.6 KB
---
name: mobile-offline-support
description: Offline-first mobile apps with local storage, sync queues, conflict resolution. Use for offline functionality, data sync, connectivity handling, or encountering sync conflicts, queue management, storage limits, network transition errors.
---

# Mobile Offline Support

Build offline-first mobile applications with local storage and synchronization.

## React Native Implementation

```javascript
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';

class OfflineManager {
  constructor() {
    this.syncQueue = [];
    this.isOnline = true;
    // Maximum items in sync queue before discarding oldest
    this.MAX_SYNC_QUEUE_LENGTH = 1000;

    NetInfo.addEventListener(state => {
      this.isOnline = state.isConnected;
      if (this.isOnline) this.processQueue();
    });
  }

  /**
   * Fetch data from server.
   * TODO: Replace with actual API endpoint implementation.
   */
  async fetchFromServer(key) {
    try {
      // Example implementation - replace with your API
      const response = await fetch(`${API_BASE_URL}/data/${key}`);
      if (!response.ok) {
        throw new Error(`Server returned ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('fetchFromServer failed:', error);
      throw new Error(`Failed to fetch ${key}: ${error.message}`);
    }
  }

  /**
   * Sync data to server.
   * TODO: Replace with actual API endpoint implementation.
   */
  async syncToServer(key, data) {
    try {
      // Example implementation - replace with your API
      const response = await fetch(`${API_BASE_URL}/data/${key}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });
      if (!response.ok) {
        throw new Error(`Server returned ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('syncToServer failed:', error);
      throw new Error(`Failed to sync ${key}: ${error.message}`);
    }
  }

  async getData(key) {
    const cached = await AsyncStorage.getItem(key);
    if (cached) return JSON.parse(cached);

    if (this.isOnline) {
      const data = await this.fetchFromServer(key);
      await AsyncStorage.setItem(key, JSON.stringify(data));
      return data;
    }

    return null;
  }

  async saveData(key, data) {
    await AsyncStorage.setItem(key, JSON.stringify(data));

    if (this.isOnline) {
      await this.syncToServer(key, data);
    } else {
      // Add to queue
      this.syncQueue.push({ key, data, timestamp: Date.now() });

      // Enforce queue bounds - discard oldest if exceeded
      while (this.syncQueue.length > this.MAX_SYNC_QUEUE_LENGTH) {
        const discarded = this.syncQueue.shift();
        console.warn(`Sync queue full - discarded oldest item: ${discarded.key}`);
      }

      // Persist trimmed queue
      await AsyncStorage.setItem('syncQueue', JSON.stringify(this.syncQueue));
    }
  }

  async processQueue() {
    const failedItems = [];
    for (const item of this.syncQueue) {
      try {
        await this.syncToServer(item.key, item.data);
      } catch (err) {
        console.error('Sync failed:', err);
        failedItems.push(item);
      }
    }
    this.syncQueue = failedItems;
    if (failedItems.length === 0) {
      await AsyncStorage.removeItem('syncQueue');
    } else {
      await AsyncStorage.setItem('syncQueue', JSON.stringify(failedItems));
    }
  }
}
```

## Conflict Resolution

```javascript
function resolveConflict(local, server) {
  // Last-write-wins
  if (local.updatedAt > server.updatedAt) return local;
  return server;

  // Or merge changes
  // return { ...server, ...local };
}
```

## UI Indicators

```jsx
function OfflineIndicator() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    return NetInfo.addEventListener(state => {
      setIsOnline(state.isConnected);
    });
  }, []);

  if (isOnline) return null;

  return (
    <View style={styles.banner}>
      <Text>You're offline. Changes will sync when connected.</Text>
    </View>
  );
}
```

## Best Practices

- Cache frequently accessed data locally
- Queue actions for later sync
- Show clear offline indicators
- Handle sync conflicts gracefully
- Compress stored data
- Test offline scenarios thoroughly

## Native Implementations

See [references/native-implementations.md](references/native-implementations.md) for:
- iOS Core Data with sync manager
- Android Room database with WorkManager sync

## Avoid

- Assuming connectivity
- Losing data on sync failures
- Unbounded queue growth
- Syncing sensitive data insecurely

Overview

This skill implements offline-first mobile support for React Native apps, combining local storage, a durable sync queue, and conflict resolution strategies. It provides patterns and ready-to-use code for caching, queuing writes while offline, and processing queued changes when connectivity returns. The goal is reliable user experience across network transitions and predictable data synchronization.

How this skill works

The skill uses AsyncStorage for local persistence and NetInfo to detect connectivity. Writes are saved locally and pushed to a sync queue when offline; the queue is persisted and processed automatically once the device is online. Conflict resolution hooks (e.g., last-write-wins or merge) are provided so you can resolve server/local inconsistencies deterministically.

When to use it

  • Apps that must work reliably with intermittent or no connectivity
  • Features where user actions should never be lost (forms, drafts, messages)
  • Mobile workflows that need background or deferred synchronization
  • Scenarios with data merge conflicts between client and server
  • When you need clear UX feedback about offline status and sync progress

Best practices

  • Cache frequently accessed reads locally to reduce network dependency
  • Queue user-initiated writes and persist the queue to survive restarts
  • Enforce a maximum queue size and discard or compact oldest items
  • Provide a visible offline indicator and clear sync/error messages
  • Implement deterministic conflict resolution and test merge cases
  • Compress or normalize stored payloads to limit storage use

Example use cases

  • Note-taking app that saves drafts offline and syncs when online
  • Field data collection where users capture entries without coverage
  • Chat or messaging client that queues outgoing messages on loss of network
  • Form-based workflows (orders, reports) with guaranteed delivery semantics
  • Background sync jobs that process persisted queues after reconnection

FAQ

How are conflicts resolved?

You can plug in strategies like last-write-wins based on updatedAt timestamps or a custom merge that combines server and local changes.

What happens if the sync queue grows too large?

The implementation enforces a max queue length and discards the oldest items, and recommends compression and queue compaction strategies to prevent unbounded growth.