Skip to content
capacitor-offline-first logo

Offline-First Capacitor Apps

capacitor-offline-first

Guide to building offline-first Capacitor apps with data synchronization, caching strategies, and conflict resolution. Covers SQLite, service workers, and network detection. Use this skill when users need their app to work without internet.

yeohj0710/wellnessbox0installs0stars

SKILL.md

Full skill instructions

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            │  │
│  │    (SQLite / IndexedDB)           │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

Network Detection

Using Capacitor Network Plugin

bun add @capacitor/​network
bunx 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 SQLite

Installation

bun add @capgo/​capacitor-data-storage-sqlite
bunx cap sync

Database Setup

import { CapacitorDataStorageSqlite } from '@capgo/​capacitor-data-storage-sqlite';

class Database {
  private db = CapacitorDataStorageSqlite;
  private isOpen = false;

  async open() {
    if (this.isOpen) return;

    await this.db.openStore({
      database: 'myapp',
      table: 'data',
      encrypted: false,
      mode: 'no-encryption',
    });

    this.isOpen = true;
  }

  async set(key: string, value: any) {
    await this.open();
    await this.db.set({
      key,
      value: JSON.stringify(value),
    });
  }

  async get<T>(key: string): Promise<T | null> {
    await this.open();
    const result = await this.db.get({ key });
    return result.value ? JSON.parse(result.value) : null;
  }

  async remove(key: string) {
    await this.open();
    await this.db.remove({ key });
  }

  async keys(): Promise<string[]> {
    await this.open();
    const result = await this.db.keys();
    return result.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
  ) {}

  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): Promise<void> {
    item.updatedAt = Date.now();
    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 response = await fetch(
      `/​api/​${repo.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' });
      } else if (localItem.syncStatus === 'synced') {
        // No local changes - update from server
        await repo.save({ ...remoteItem, syncStatus: 'synced' });
      } 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' });
    }
  }
}

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

More skills from yeohj0710

capacitor-testing logo
yeohj0710/wellnessbox

capacitor-testing

Complete testing guide for Capacitor apps covering unit tests, integration tests, E2E tests, and native testing. Includes Jest, Vitest, Playwright, Appium, and native testing frameworks. Use this skill when users need to test their mobile apps.

0 0
View
vercel-composition-patterns logo
yeohj0710/wellnessbox

vercel-composition-patterns

Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.

0 0
View

Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.

0 0
View
next-upgrade logo
yeohj0710/wellnessbox

next-upgrade

Upgrade Next.js to the latest version following official migration guides and codemods

0 0
View
capacitor-deep-linking logo
yeohj0710/wellnessbox

capacitor-deep-linking

Complete guide to implementing deep links and universal links in Capacitor apps. Covers iOS Universal Links, Android App Links, custom URL schemes, and navigation handling. Use this skill when users need to open their app from links.

0 0
View
tailwind-capacitor logo
yeohj0710/wellnessbox

tailwind-capacitor

Guide to using Tailwind CSS in Capacitor mobile apps. Covers mobile-first design, touch targets, safe areas, dark mode, and performance optimization. Use this skill when users want to style Capacitor apps with Tailwind.

0 0
View
vercel-deploy logo
yeohj0710/wellnessbox

vercel-deploy

Deploy applications and websites to Vercel. Use when the user requests deployment actions like "deploy my app", "deploy and give me the link", "push this live", or "create a preview deployment".

0 0
View
capacitor-keyboard logo
yeohj0710/wellnessbox

capacitor-keyboard

Guide to handling keyboard in Capacitor apps including visibility detection, accessory bar, scroll behavior, and input focus. Use this skill when users have keyboard-related issues.

0 0
View
frontend-design logo
yeohj0710/wellnessbox

frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beaut...

0 0
View
ios-android-logs logo
yeohj0710/wellnessbox

ios-android-logs

Guide to accessing device logs on iOS and Android for Capacitor apps. Covers command-line tools, GUI applications, filtering, and real-time streaming. Use this skill when users need to view device logs for debugging.

0 0
View
vercel-react-native-skills logo
yeohj0710/wellnessbox

vercel-react-native-skills

Comprehensive best practices for React Native and Expo applications. Contains rules across multiple categories covering performance, animations, UI patterns, and platform-specific optimizations.

0 0
View
develop-ai-functions-example logo
yeohj0710/wellnessbox

develop-ai-functions-example

Develop examples for AI SDK functions. Use when creating, running, or modifying examples under examples/ai-functions/src to validate provider support, demonstrate features, or create test fixtures.

0 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View