Skip to main content
skill CX Codex

mobile-offline-support · secondsky — Claude Code skill: offline-first mobile apps

A skill for offline-first mobile apps: local storage, a sync queue and conflict resolution, so the agent stops assuming the network is always there.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add secondsky/claude-skills --skill mobile-offline-support -a claude-code
Open on GitHub

What mobile-offline-support does

The skill hands the agent a working pattern for data that has to survive a dropped connection. At its core is an OfflineManager class for React Native: reads and writes go through AsyncStorage, and NetInfo reports whether the device is online.

When the connection is gone, changes land in a sync queue instead of being sent. The queue is capped by MAX_SYNC_QUEUE_LENGTH, set to 1000 — past that, the oldest entries get dropped with a console warning. Conflicts are settled by resolveConflict using last-write-wins, with a commented-out merge variant if you prefer that. An OfflineIndicator component renders a banner while the device is offline.

Both fetchFromServer and syncToServer carry TODO markers. They are examples, not a client. Point them at your own endpoints and API_BASE_URL before trusting a single request. Native paths for iOS Core Data and Android Room with WorkManager live in references/native-implementations.md.

What it helps with

  • keep form data when the connection drops
  • flush queued changes once the device is back online
  • settle a conflict between local and server copies of a record
  • tell the user the app is running offline
  • cap how far the sync queue can grow

Example prompts for the agent

  • Add offline support to the checkout screen with local storage and a sync queue
  • Why do sync conflicts overwrite newer data? Walk through resolveConflict
  • Add an offline banner using OfflineIndicator

How to install mobile-offline-support

1Skills CLI (Vercel)

npx skills add secondsky/claude-skills --skill mobile-offline-support -a claude-code

2Manually: put the files in place

~/.claude/skills/mobile-offline-support/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/mobile-offline-support/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about mobile-offline-support

How do I install mobile-offline-support in Claude?

Run npx skills add secondsky/claude-skills --skill mobile-offline-support -a claude-code. The skill lands in ~/.claude/skills/mobile-offline-support/SKILL.md.

What does mobile-offline-support do?

It gives the agent patterns for offline-first mobile apps: caching in local storage, a deferred sync queue, conflict resolution and an offline indicator.

What breaks in mobile-offline-support?

fetchFromServer and syncToServer are stubs marked TODO. Without your own endpoints and API_BASE_URL, nothing syncs.

Source text

SKILL.md · MIT
---
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.
license: MIT
---

# 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

© secondsky · MIT · secondsky/claude-skills

Similar skills

Everything for Mobile →

mobile-offline-support by other authors comes first.

From the same set94

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor