Skip to main content
capacitor-offline-first Guide to building offline-first Capacitor apps with data synchronization, caching strategies, and conflict resolution. Covers Fast SQL, service workers, and network detection. Use this skill when users need their app to work without internet.
Ir a la instalación Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/Cap-go/capgo-skills --skill capacitor-offline-firstEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando... Más de este repositorio capawesome-live-update-migration Guides migration from Capawesome Cloud live updates or @capawesome/capacitor-live-update to Capgo Updater. Use when a Capacitor app contains Capawesome live update packages, CLI commands, config, API calls, or when the user asks why Capgo Updater is the better live-update path: native updater runtime, fully open source, cheaper at comparable scale, and longer proven track record.
Official Capacitor package guide plus Capgo ecosystem plugin recommendations. Use this skill when users need native functionality, want the right official Capacitor package, or need a stronger Capgo/community plugin when the official package is missing or too limited.
Use for Capgo Cloud Build native iOS and Android workflows, including CLI login, API-key handling, iOS build onboarding, signing credential storage, build requests, store upload settings, output download links, and troubleshooting. Do not use for OTA bundle uploads or generic Capacitor setup unless a native Capgo build is requested.
Explorador de archivos
2 archivos Ocupaciones relacionadas SOC
Basado en la clasificación ocupacional SOC
name capacitor-offline-first description Guide to building offline-first Capacitor apps with data synchronization, caching strategies, and conflict resolution. Covers Fast SQL, service workers, and network detection. Use this skill when users need their app to work without internet.
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' ;
const status = await Network . ();
. ( , status. );
. ( , status. );
. ( , {
. ( , status. );
(status. ) {
syncManager. ();
} {
();
}
});
getStatus
console
log
'Connected:'
connected
console
log
'Connection type:'
connectionType
Network
addListener
'networkStatusChange'
(status ) =>
console
log
'Network status changed:'
connected
if
connected
syncPendingChanges
else
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 this .getCachedData (url);
}
try {
const response = await fetch (url, options);
const data = await response.json ();
await this .cacheData (url, data);
return data;
} catch (error) {
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 > {
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);
}
}
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) {
await repo.save ({ ...remoteItem, syncStatus : 'synced' }, { markPending : false });
} else if (localItem.syncStatus === 'synced' ) {
await repo.save ({ ...remoteItem, syncStatus : 'synced' }, { markPending : false });
} else {
await this .resolveConflict (localItem, remoteItem, repo);
}
}
await this .setLastSyncTime (repo, Date .now ());
}
private async resolveConflict (
local : any ,
remote : any ,
repo : OfflineRepository <any >
) {
if (local.updatedAt > remote.updatedAt ) {
local.syncStatus = 'pending' ;
await repo.save (local);
} else {
await repo.save ({ ...remote, syncStatus : 'synced' }, { markPending : false });
}
}
}
Service Worker Caching
Register Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker .register ('/sw.js' );
}
Service Worker with Workbox
import { precacheAndRoute } from 'workbox-precaching' ;
import { registerRoute } from 'workbox-routing' ;
import { StaleWhileRevalidate , CacheFirst , NetworkFirst } from 'workbox-strategies' ;
precacheAndRoute (self.__WB_MANIFEST );
registerRoute (
({ url } ) => url.pathname .startsWith ('/api/' ),
new NetworkFirst ({
cacheName : 'api-cache' ,
networkTimeoutSeconds : 5 ,
})
);
registerRoute (
({ request } ) => request.destination === 'image' ,
new CacheFirst ({
cacheName : 'image-cache' ,
plugins : [
{
expiration : {
maxEntries : 100 ,
maxAgeSeconds : 7 * 24 * 60 * 60 ,
},
},
],
})
);
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' ,
};
await this .repo .save (todo);
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) {
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 > {
return local.updatedAt > remote.updatedAt ? local : remote;
return {
...remote,
...local,
updatedAt : Math .max (local.updatedAt , remote.updatedAt ),
};
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' );
}
}
Resources