Skip to main content

offline-first

Mobile architecture designed for offline functionality

Source facts

Repository
ffsshhttiikk/opencode-agents-skills
Last source activity
February 28, 2026 at 22:53
Detected SKILL.md language
English
Stars
3
Forks
2

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
offline-first
description
Mobile architecture designed for offline functionality
category
mobile-development
difficulty
advanced
tags
["mobile","offline","sync","local-storage"]
author
OpenCode Community
version
1
last_updated
2024-01-15T00:00:00.000Z
# Offline-First Architecture ## What I Do I am Offline-First Architecture, a design approach that prioritizes application functionality regardless of network connectivity. I assume network availability is the exception rather than the norm, designing applications to work seamlessly offline with data stored locally. I synchronize local changes with remote servers when connectivity is restored using conflict resolution strategies. I implement various storage mechanisms including SQLite, Realm, IndexedDB, and file systems. I handle background sync, push notifications for sync completion, and merge strategies for concurrent edits. I optimize for low-bandwidth scenarios, progressive data loading, and graceful degradation. I enable users to be productive during network outages while ensuring data consistency upon reconnection. ## When to Use Me - Mobile applications with unreliable connectivity - Field service and enterprise mobile apps - Productivity applications requiring data access anywhere - IoT and embedded device applications - Applications with heavy data consumption patterns - User-facing scenarios with variable network conditions - Applications requiring immediate responsiveness - Compliance scenarios requiring local data audit trails ## Core Concepts **Local Storage**: Primary data store that persists data locally on the device. **Sync Queue**: FIFO queue managing pending changes awaiting synchronization. **Conflict Resolution**: Strategies for merging concurrent local and remote edits. **Background Sync**: Automatic synchronization when app is not active. **Data Versioning**: Tracking data states with timestamps or version numbers. **Optimistic UI**: Immediate user feedback assuming successful operations. **Delta Sync**: Transferring only changed data rather than full datasets. **Connectivity Awareness**: Real-time detection of network state changes. ## Code Examples ### Example 1: Offline Data Store with Conflict Resolution ```typescript // offline-store.ts abstract class OfflineStore<T> { protected abstract createLocalStorage(): LocalStorage protected abstract createSyncQueue(): SyncQueue protected abstract createConflictResolver(): ConflictResolver<T> async save(entity: T): Promise<void> { const localStorage = this.createLocalStorage() const syncQueue = this.createSyncQueue() // Optimistically save to local storage await localStorage.save(entity) // Queue for sync await syncQueue.enqueue({ type: 'UPSERT', entity, timestamp: Date.now(), version: entity.version || 0 }) } async delete(id: string): Promise<void> { const localStorage = this.createLocalStorage() const syncQueue = this.createSyncQueue() await localStorage.delete(id) await syncQueue.enqueue({ type: 'DELETE', entityId: id, timestamp: Date.now() }) } async get(id: string): Promise<T | null> { const localStorage = this.createLocalStorage() return localStorage.get(id) } async getAll(): Promise<T[]> { const localStorage = this.createLocalStorage() return localStorage.getAll() } async sync(): Promise<SyncResult> { const localStorage = this.createLocalStorage() const syncQueue = this.createSyncQueue() const conflictResolver = this.createConflictResolver() const pendingChanges = await syncQueue.getPending() const syncResult: SyncResult = { success: 0, failed: 0, conflicts: [] } for (const change of pendingChanges) { try { const remoteEntity = await this.fetchRemote(change.entityId) if (remoteEntity && change.type === 'UPSERT') { const conflict = await conflictResolver.resolve( change.entity, remoteEntity ) if (conflict.resolution === 'MERGE') { const mergedEntity = conflict.merged! await this.updateRemote(mergedEntity) await localStorage.save(mergedEntity) } else if (conflict.resolution === 'REMOTE') { await localStorage.save(remoteEntity) await syncQueue.remove(change.id) } syncResult.conflicts.push(conflict) } else { await this.updateRemote(change.entity) await syncQueue.remove(change.id) } syncResult.success++ } catch (error) { syncResult.failed++ await syncQueue.markFailed(change.id, error) } } return syncResult } protected abstract fetchRemote(id: string): Promise<T | null> protected abstract updateRemote(entity: T): Promise<void> } interface SyncQueueItem { id: string type: 'UPSERT' | 'DELETE' entity?: T entityId?: string timestamp: number version: number retryCount: number status: 'pending' | 'synced' | 'failed' } interface SyncResult { success: number failed: number conflicts: Conflict<T>[] } interface Conflict<T> { local: T remote: T resolution: 'LOCAL' | 'REMOTE' | 'MERGE' merged?: T } ``` ### Example 2: SQLite Database with React Native ```typescript // database.ts import SQLite from 'react-native-sqlite-storage' const DATABASE_NAME = 'offline_app.db' interface Database { open(): Promise<void> close(): Promise<void> execute(sql: string, params?: any[]): Promise<any> transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> } interface Transaction { executeSql(sql: string, params?: any[]): Promise<{ rows: any[], insertId: number }> } class SQLiteDatabase implements Database { private db: SQLite.SQLiteDatabase | null = null async open(): Promise<void> { this.db = await SQLite.openDatabase({ name: DATABASE_NAME, location: 'default', createFromLocation: '~www/database.db', }) await this.createTables() } private async createTables(): Promise<void> { await this.execute(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, version INTEGER DEFAULT 1, created_at INTEGER DEFAULT (unixepoch()), updated_at INTEGER DEFAULT (unixepoch()), sync_status TEXT DEFAULT 'pending', is_deleted INTEGER DEFAULT 0 ) `) await this.execute(` CREATE TABLE IF NOT EXISTS posts ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, title TEXT NOT NULL, body TEXT, version INTEGER DEFAULT 1, created_at INTEGER DEFAULT (unixepoch()), updated_at INTEGER DEFAULT (unixepoch()), sync_status TEXT DEFAULT 'pending', is_deleted INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ) `) await this.execute(` CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id) `) } async execute(sql: string, params?: any[]): Promise<any> { if (!this.db) throw new Error('Database not opened') return new Promise((resolve, reject) => { this.db!.executeSql(sql, params).then(([results]) => { resolve({ rows: results.rows, insertId: results.insertId }) }).catch(reject) }) } async transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> { if (!this.db) throw new Error('Database not opened') return new Promise((resolve, reject) => { this.db!.transaction(tx => { fn({ executeSql: (sql, params) => { return new Promise((resolve, reject) => { tx.executeSql(sql, params, (_, results) => { resolve({ rows: results.rows, insertId: results.insertId }) }).catch(reject) }) } }) }).then(resolve).catch(reject) }) } async close(): Promise<void> { if (this.db) { await SQLite.closeDatabase(this.db) this.db = null } } } // Repository implementation class UserRepository { private db: SQLiteDatabase constructor(db: SQLiteDatabase) { this.db = db } async create(user: User): Promise<void> { await this.db.execute( `INSERT INTO users (id, name, email, version, sync_status) VALUES (?, ?, ?, 1, 'pending')`, [user.id, user.name, user.email] ) } async update(user: User): Promise<void> { await this.db.execute( `UPDATE users SET name = ?, email = ?, version = version + 1, updated_at = unixepoch(), sync_status = 'pending' WHERE id = ?`, [user.name, user.email, user.id] ) } async softDelete(id: string): Promise<void> { await this.db.execute( `UPDATE users SET is_deleted = 1, updated_at = unixepoch(), sync_status = 'pending' WHERE id = ?`, [id] ) } async getById(id: string): Promise<User | null> { const result = await this.db.execute( 'SELECT * FROM users WHERE id = ? AND is_deleted = 0', [id] ) return result.rows.length > 0 ? result.rows.item(0) : null } async getAll(): Promise<User[]> { const result = await this.db.execute( 'SELECT * FROM users WHERE is_deleted = 0 ORDER BY updated_at DESC' ) return result.rows.raw() } async getPendingSync(): Promise<User[]> { const result = await this.db.execute( 'SELECT * FROM users WHERE sync_status = ? AND is_deleted = 0', ['pending'] ) return result.rows.raw() } async markSynced(id: string): Promise<void> { await this.db.execute( 'UPDATE users SET sync_status = ? WHERE id = ?', ['synced', id] ) } } ``` ### Example 3: Network Connectivity Monitor ```typescript // connectivity.ts import { NetInfoState, NetInfoSubscription, addEventListener } from '@react-native-netinfo/ios' type ConnectivityChangeListener = (isConnected: boolean, details: NetInfoState) => void class ConnectivityMonitor { private listeners: Set<ConnectivityChangeListener> = new Set() private subscription: NetInfoSubscription | null = null private currentState: NetInfoState | null = null startMonitoring(): void { this.subscription = addEventListener((state) => { this.currentState = state const isConnected = state.isConnected this.listeners.forEach(listener => listener(isConnected, state)) }) } stopMonitoring(): void { if (this.subscription) { this.subscription() this.subscription = null } } addListener(listener: ConnectivityChangeListener): () => void { this.listeners.add(listener) return () => this.listeners.delete(listener) } getCurrentState(): NetInfoState | null { return this.currentState } isConnected(): boolean { return this.currentState?.isConnected || false } getConnectionType(): string { return this.currentState?.type || 'unknown' } isWifi(): boolean { return this.currentState?.type === 'wifi' } isCellular(): boolean { return this.currentState?.type === 'cellular' } } // React hook for connectivity function useConnectivity(): { isConnected: boolean; connectionType: string } { const [connectivity, setConnectivity] = useState({ isConnected: true, connectionType: 'unknown' }) useEffect(() => { const monitor = new ConnectivityMonitor() const unsubscribe = monitor.addListener((isConnected, state) => { setConnectivity({ isConnected, connectionType: state.type }) }) monitor.startMonitoring() return () => { unsubscribe() monitor.stopMonitoring() } }, []) return connectivity } ``` ### Example 4: Background Sync Worker ```typescript // background-sync.ts import { AppState } from 'react-native' import BackgroundTask from 'react-native-background-fetch' class BackgroundSyncManager { private isSyncing = false private pendingSync = false async initialize(): Promise<void> { const status = await BackgroundTask.configure({ minimumFetchInterval: 15, stopOnTerminate: false, startOnBoot: true, requiredNetworkType: BackgroundTask.NETWORK_TYPE_ANY, }) if (status === BackgroundTask.STATUS_AVAILABLE) { await BackgroundTask.registerBackgroundTask( 'sync-task', this.performSync.bind(this) ) } } async performSync(): Promise<void> { if (this.isSyncing) { this.pendingSync = true return } this.isSyncing = true try { const connectivity = new ConnectivityMonitor() if (!connectivity.isConnected()) { return } const syncManager = new SyncManager() const result = await syncManager.syncAll() console.log('Background sync completed:', result) if (result.failed > 0) { await this.scheduleRetry(result.failed) } } catch (error) { console.error('Background sync failed:', error) } finally { this.isSyncing = false if (this.pendingSync) { this.pendingSync = false await this.performSync() } } BackgroundTask.finish('sync-task') } private async scheduleRetry(failedCount: number): Promise<void> { // Exponential backoff for retries const delay = Math.min(1000 * Math.pow(2, failedCount), 300000) await new Promise(resolve => setTimeout(resolve, delay)) await BackgroundTask.registerBackgroundTask( 'retry-sync-task', this.performSync.bind(this) ) } } // Sync Manager with queue processing class SyncManager { private userRepository: UserRepository private postRepository: PostRepository private conflictResolver: ConflictResolver async syncAll(): Promise<SyncResult> { const result: SyncResult = { success: 0, failed: 0, conflicts: 0 } // Sync users const userResult = await this.syncUsers() result.success += userResult.success result.failed += userResult.failed result.conflicts += userResult.conflicts // Sync posts const postResult = await this.syncPosts() result.success += postResult.success result.failed += postResult.failed
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub