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 GitHubThis SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub