Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    cap-go

    capacitor-offline-first

    cap-go/capacitor-offline-first
    Coding
    3

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Guide to building offline-first Capacitor apps with data synchronization, caching strategies, and conflict resolution. Covers SQLite, service workers, and network detection...

    SKILL.md

    Offline-First Capacitor Apps

    Build apps that work seamlessly with or without internet connectivity.

    When to Use This Skill

    • User needs offline support
    • User asks about data sync
    • User wants caching
    • User needs local database
    • User has connectivity issues

    Offline-First Architecture

    ┌─────────────────────────────────────────┐
    │              UI Layer                    │
    ├─────────────────────────────────────────┤
    │           Service Layer                  │
    │  ┌─────────────┐  ┌─────────────────┐   │
    │  │ Online Mode │  │ Offline Mode    │   │
    │  └──────┬──────┘  └────────┬────────┘   │
    ├─────────┼──────────────────┼────────────┤
    │         │    Sync Manager  │            │
    │         └────────┬─────────┘            │
    ├──────────────────┼──────────────────────┤
    │  ┌───────────────┴───────────────────┐  │
    │  │         Local Database            │  │
    │  │   (Fast SQL / IndexedDB)          │  │
    │  └───────────────────────────────────┘  │
    └─────────────────────────────────────────┘
    

    Network Detection

    Using Capacitor Network Plugin

    npm install @capacitor/network
    npx cap sync
    
    import { Network } from '@capacitor/network';
    
    // Check current status
    const status = await Network.getStatus();
    console.log('Connected:', status.connected);
    console.log('Connection type:', status.connectionType);
    
    // Listen for changes
    Network.addListener('networkStatusChange', (status) => {
      console.log('Network status changed:', status.connected);
    
      if (status.connected) {
        // Back online - sync data
        syncManager.syncPendingChanges();
      } else {
        // Offline - show indicator
        showOfflineIndicator();
      }
    });
    

    Network-Aware Service

    import { Network } from '@capacitor/network';
    
    class NetworkAwareService {
      private isOnline = true;
    
      constructor() {
        this.init();
      }
    
      private async init() {
        const status = await Network.getStatus();
        this.isOnline = status.connected;
    
        Network.addListener('networkStatusChange', (status) => {
          this.isOnline = status.connected;
        });
      }
    
      async fetch<T>(url: string, options?: RequestInit): Promise<T> {
        if (!this.isOnline) {
          // Return cached data
          return this.getCachedData(url);
        }
    
        try {
          const response = await fetch(url, options);
          const data = await response.json();
    
          // Cache the response
          await this.cacheData(url, data);
    
          return data;
        } catch (error) {
          // Network error - try cache
          return this.getCachedData(url);
        }
      }
    }
    

    Local Database with Fast SQL

    Installation

    npm install @capgo/capacitor-fast-sql
    npx cap sync
    

    Before using Fast SQL in production, complete the required platform setup:

    • iOS: allow localhost networking for the plugin transport.
    • Android: add the localhost cleartext exception required by the plugin.
    • Web: install sql.js if the app needs the web fallback.

    Use the dedicated sqlite-to-fast-sql skill when you need the full platform checklist.

    Database Setup

    import { KeyValueStore } from '@capgo/capacitor-fast-sql';
    
    class Database {
      private store: Awaited<ReturnType<typeof KeyValueStore.open>> | null = null;
    
      async open() {
        if (this.store) return;
        this.store = await KeyValueStore.open({
          database: 'myapp',
          store: 'data',
          encrypted: false,
        });
      }
    
      async set(key: string, value: any) {
        await this.open();
        await this.store!.set(key, value);
      }
    
      async get<T>(key: string): Promise<T | null> {
        await this.open();
        return this.store!.get<T>(key);
      }
    
      async remove(key: string) {
        await this.open();
        await this.store!.remove(key);
      }
    
      async keys(): Promise<string[]> {
        await this.open();
        return this.store!.keys();
      }
    }
    

    Offline Data Repository

    interface Entity {
      id: string;
      updatedAt: number;
      syncStatus: 'synced' | 'pending' | 'conflict';
    }
    
    class OfflineRepository<T extends Entity> {
      constructor(
        private db: Database,
        private collection: string
      ) {}
    
      getCollection(): string {
        return this.collection;
      }
    
      async getAll(): Promise<T[]> {
        const keys = await this.db.keys();
        const items: T[] = [];
    
        for (const key of keys) {
          if (key.startsWith(`${this.collection}:`)) {
            const item = await this.db.get<T>(key);
            if (item) items.push(item);
          }
        }
    
        return items;
      }
    
      async getById(id: string): Promise<T | null> {
        return this.db.get<T>(`${this.collection}:${id}`);
      }
    
      async save(item: T, options?: { markPending?: boolean }): Promise<void> {
        item.updatedAt = Date.now();
        if (options?.markPending ?? true) {
          item.syncStatus = 'pending';
        }
        await this.db.set(`${this.collection}:${item.id}`, item);
      }
    
      async delete(id: string): Promise<void> {
        // Soft delete - mark for sync
        const item = await this.getById(id);
        if (item) {
          item.syncStatus = 'pending';
          (item as any).deleted = true;
          await this.db.set(`${this.collection}:${id}`, item);
        }
      }
    
      async getPending(): Promise<T[]> {
        const all = await this.getAll();
        return all.filter((item) => item.syncStatus === 'pending');
      }
    
      async markSynced(id: string): Promise<void> {
        const item = await this.getById(id);
        if (item) {
          item.syncStatus = 'synced';
          await this.db.set(`${this.collection}:${id}`, item);
        }
      }
    }
    

    Sync Manager

    import { Network } from '@capacitor/network';
    
    class SyncManager {
      private isSyncing = false;
      private syncQueue: Array<() => Promise<void>> = [];
    
      constructor(private repositories: OfflineRepository<any>[]) {
        this.setupNetworkListener();
      }
    
      private setupNetworkListener() {
        Network.addListener('networkStatusChange', async (status) => {
          if (status.connected) {
            await this.syncAll();
          }
        });
      }
    
      async syncAll() {
        if (this.isSyncing) return;
        this.isSyncing = true;
    
        try {
          for (const repo of this.repositories) {
            await this.syncRepository(repo);
          }
        } finally {
          this.isSyncing = false;
        }
      }
    
      private async syncRepository(repo: OfflineRepository<any>) {
        const pending = await repo.getPending();
    
        for (const item of pending) {
          try {
            if ((item as any).deleted) {
              await this.deleteRemote(item);
            } else {
              await this.syncToRemote(item);
            }
            await repo.markSynced(item.id);
          } catch (error) {
            console.error('Sync failed for item:', item.id, error);
            // Keep as pending for retry
          }
        }
    
        // Pull remote changes
        await this.pullRemoteChanges(repo);
      }
    
      private async syncToRemote(item: any) {
        await fetch(`/api/${item.collection}/${item.id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(item),
        });
      }
    
      private async deleteRemote(item: any) {
        await fetch(`/api/${item.collection}/${item.id}`, {
          method: 'DELETE',
        });
      }
    
      private async pullRemoteChanges(repo: OfflineRepository<any>) {
        const lastSync = await this.getLastSyncTime(repo);
        const collection = repo.getCollection();
        const response = await fetch(
          `/api/${collection}?since=${lastSync}`
        );
        const remoteItems = await response.json();
    
        for (const remoteItem of remoteItems) {
          const localItem = await repo.getById(remoteItem.id);
    
          if (!localItem) {
            // New item from server
            await repo.save({ ...remoteItem, syncStatus: 'synced' }, { markPending: false });
          } else if (localItem.syncStatus === 'synced') {
            // No local changes - update from server
            await repo.save({ ...remoteItem, syncStatus: 'synced' }, { markPending: false });
          } else {
            // Conflict - local has pending changes
            await this.resolveConflict(localItem, remoteItem, repo);
          }
        }
    
        await this.setLastSyncTime(repo, Date.now());
      }
    
      private async resolveConflict(
        local: any,
        remote: any,
        repo: OfflineRepository<any>
      ) {
        // Last-write-wins strategy
        if (local.updatedAt > remote.updatedAt) {
          // Keep local, re-sync to server
          local.syncStatus = 'pending';
          await repo.save(local);
        } else {
          // Server wins
          await repo.save({ ...remote, syncStatus: 'synced' }, { markPending: false });
        }
      }
    }
    

    Service Worker Caching

    Register Service Worker

    // src/main.ts
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js');
    }
    

    Service Worker with Workbox

    // public/sw.js
    import { precacheAndRoute } from 'workbox-precaching';
    import { registerRoute } from 'workbox-routing';
    import { StaleWhileRevalidate, CacheFirst, NetworkFirst } from 'workbox-strategies';
    
    // Precache static assets
    precacheAndRoute(self.__WB_MANIFEST);
    
    // Cache API responses
    registerRoute(
      ({ url }) => url.pathname.startsWith('/api/'),
      new NetworkFirst({
        cacheName: 'api-cache',
        networkTimeoutSeconds: 5,
      })
    );
    
    // Cache images
    registerRoute(
      ({ request }) => request.destination === 'image',
      new CacheFirst({
        cacheName: 'image-cache',
        plugins: [
          {
            expiration: {
              maxEntries: 100,
              maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
            },
          },
        ],
      })
    );
    
    // Cache fonts
    registerRoute(
      ({ request }) => request.destination === 'font',
      new CacheFirst({
        cacheName: 'font-cache',
      })
    );
    

    Optimistic UI Updates

    class TodoService {
      constructor(
        private repo: OfflineRepository<Todo>,
        private syncManager: SyncManager
      ) {}
    
      async addTodo(text: string): Promise<Todo> {
        const todo: Todo = {
          id: crypto.randomUUID(),
          text,
          completed: false,
          updatedAt: Date.now(),
          syncStatus: 'pending',
        };
    
        // Save locally immediately
        await this.repo.save(todo);
    
        // Trigger sync in background
        this.syncManager.syncAll().catch(console.error);
    
        return todo;
      }
    
      async toggleComplete(id: string): Promise<Todo> {
        const todo = await this.repo.getById(id);
        if (!todo) throw new Error('Todo not found');
    
        todo.completed = !todo.completed;
        await this.repo.save(todo);
    
        this.syncManager.syncAll().catch(console.error);
    
        return todo;
      }
    }
    

    Queue Failed Requests

    class RequestQueue {
      private queue: QueuedRequest[] = [];
    
      constructor(private storage: Database) {
        this.loadQueue();
      }
    
      private async loadQueue() {
        this.queue = await this.storage.get<QueuedRequest[]>('requestQueue') || [];
      }
    
      private async saveQueue() {
        await this.storage.set('requestQueue', this.queue);
      }
    
      async enqueue(request: QueuedRequest) {
        this.queue.push(request);
        await this.saveQueue();
      }
    
      async processQueue() {
        const status = await Network.getStatus();
        if (!status.connected) return;
    
        while (this.queue.length > 0) {
          const request = this.queue[0];
    
          try {
            await fetch(request.url, {
              method: request.method,
              headers: request.headers,
              body: request.body,
            });
    
            this.queue.shift();
            await this.saveQueue();
          } catch (error) {
            // Stop processing on failure
            break;
          }
        }
      }
    }
    

    Best Practices

    1. Show Sync Status

    function SyncIndicator() {
      const { isOnline, pendingChanges, isSyncing } = useSyncStatus();
    
      if (!isOnline) {
        return <Badge color="warning">Offline</Badge>;
      }
    
      if (isSyncing) {
        return <Badge color="info">Syncing...</Badge>;
      }
    
      if (pendingChanges > 0) {
        return <Badge color="warning">{pendingChanges} pending</Badge>;
      }
    
      return <Badge color="success">Synced</Badge>;
    }
    

    2. Handle Conflicts Gracefully

    async function handleConflict(local: Todo, remote: Todo): Promise<Todo> {
      // Option 1: Last write wins
      return local.updatedAt > remote.updatedAt ? local : remote;
    
      // Option 2: Merge changes
      return {
        ...remote,
        ...local,
        updatedAt: Math.max(local.updatedAt, remote.updatedAt),
      };
    
      // Option 3: Ask user
      const choice = await showConflictDialog(local, remote);
      return choice === 'local' ? local : remote;
    }
    

    3. Validate Before Sync

    function validateTodo(todo: Todo): boolean {
      if (!todo.id || !todo.text) return false;
      if (todo.text.length > 500) return false;
      return true;
    }
    
    async function syncTodo(todo: Todo) {
      if (!validateTodo(todo)) {
        throw new Error('Invalid todo');
      }
      // Proceed with sync
    }
    

    Resources

    • Capacitor Network: https://capacitorjs.com/docs/apis/network
    • Workbox: https://developer.chrome.com/docs/workbox
    • IndexedDB: https://developer.mozilla.org/docs/Web/API/IndexedDB_API
    • Offline First Manifesto: http://offlinefirst.org
    Recommended Servers
    GroundRoute: Web Search for AI Agents across 6 Engines ($10 free credit)
    GroundRoute: Web Search for AI Agents across 6 Engines ($10 free credit)
    ThinAir Data
    ThinAir Data
    DC Hub — Data Center & Energy Intelligence
    DC Hub — Data Center & Energy Intelligence
    Repository
    cap-go/capacitor-skills
    Files