Offline-First Strategy

Offline-First Strategy 
 
 Project: {{PROJECT_NAME}}
 Version: {{VERSION}}
 Date: {{DATE}}
 Author: {{AUTHOR}}
 Status: Draft | In Review | Approved
 Reviewers: {{REVIEWERS}} 
 
 Document History 
 
 
 
 Version 
 Date 
 Author 
 Changes 
 
 
 
 
 0.1 
 {{DATE}} 
 {{AUTHOR}} 
 Initial draft 
 
 
 
 
 1. Offline Capability Requirements 

 
 
 
 Feature 
 Offline Support 
 Priority 
 Notes 
 
 
 
 
 View cached content feed 
 Required 
 P1 
 Last 50 items 
 
 
 Create draft (saved locally) 
 Required 
 P1 
 Sync when online 
 
 
 Search (local cache only) 
 Partial 
 P2 
 Degraded — no remote results 
 
 
 User authentication 
 Not required 
 — 
 Login requires network 
 
 
 Push notifications 
 N/A 
 — 
 Requires network by nature 
 
 
 File uploads 
 Queue 
 P2 
 Upload when network returns 
 
 
 {{FEATURE}} 
 {{Required/Partial/Not required}} 
 {{P1-P3}} 
 {{Notes}} 
 
 
 
 Offline minimum viable experience: 
 
 TODO: Define what the user should see/do when completely offline — blank screen, cached data, or read-only mode. 
 
 
 2. Local Storage Architecture 

 2.1 Database (Structured Data) 
 Selected database: {{WatermelonDB | SQLite (expo-sqlite) | Realm | TinyBase}} 
 Rationale: 
 
 TODO: Explain why this DB was chosen (query capability, sync support, performance, bundle size). 
 
 Schema overview: 
 -- Example schema — expand per domain

CREATE TABLE users (
 id TEXT PRIMARY KEY,
 name TEXT NOT NULL,
 email TEXT UNIQUE NOT NULL,
 avatar_url TEXT,
 synced_at INTEGER,
 updated_at INTEGER NOT NULL
);

CREATE TABLE posts (
 id TEXT PRIMARY KEY,
 user_id TEXT REFERENCES users(id),
 title TEXT NOT NULL,
 body TEXT,
 status TEXT DEFAULT 'published',
 is_local_draft INTEGER DEFAULT 0,
 synced_at INTEGER,
 created_at INTEGER NOT NULL,
 updated_at INTEGER NOT NULL
);

CREATE TABLE sync_queue (
 id TEXT PRIMARY KEY,
 entity_type TEXT NOT NULL,
 entity_id TEXT NOT NULL,
 operation TEXT NOT NULL, -- 'create' | 'update' | 'delete'
 payload TEXT NOT NULL, -- JSON
 retry_count INTEGER DEFAULT 0,
 created_at INTEGER NOT NULL
);
 
 TODO: Define full schema for all entities. 
 
 2.2 File Storage 
 
 
 
 Type 
 Location 
 Max Size 
 Eviction 
 
 
 
 
 Downloaded images 
 {{FileSystem.cacheDirectory}}/images/ 
 200 MB 
 LRU on cache full 
 
 
 Downloaded documents 
 {{FileSystem.documentDirectory}}/docs/ 
 500 MB 
 Manual user delete 
 
 
 Queued upload files 
 {{FileSystem.documentDirectory}}/uploads/ 
 1 GB 
 On successful upload 
 
 
 Temporary files 
 {{FileSystem.cacheDirectory}}/tmp/ 
 50 MB 
 On app start 
 
 
 
 Library: {{expo-file-system | react-native-fs}} 
 
 2.3 Secure Storage 
 
 
 
 Data 
 Storage 
 Reason 
 
 
 
 
 Auth token 
 {{expo-secure-store}} 
 Encrypted, Keychain/Keystore 
 
 
 Refresh token 
 {{expo-secure-store}} 
 Encrypted 
 
 
 Encryption key (for local DB) 
 {{expo-secure-store}} 
 Never in plain storage 
 
 
 User preferences 
 AsyncStorage 
 Non-sensitive 
 
 
 
 Rule: Anything accessed without a password must NOT be in secure storage (breaks biometric auth flow). 
 
 3. Sync Protocol Design 

 sequenceDiagram
 participant App
 participant LocalDB
 participant SyncQueue
 participant API

 Note over App,API: Online sync cycle

 App->>LocalDB: Read local data (immediate)
 App->>SyncQueue: Queue local changes
 App->>API: Push: POST /sync/push {changes}
 API-->>App: Server-applied changes + conflicts
 App->>LocalDB: Apply server changes

 App->>API: Pull: GET /sync/pull?since={timestamp}
 API-->>App: Remote changes since last pull
 App->>LocalDB: Merge remote changes

 Note over App,API: Offline scenario

 App->>LocalDB: Read cached data
 App->>SyncQueue: Queue changes (persisted)
 Note over SyncQueue: Waits for connectivity

 Note over App,API: Reconnect

 SyncQueue->>API: Drain queue — push all pending
 API-->>App: Conflict resolution
 App->>LocalDB: Merge resolved state
 
 3.1 Sync Strategy 
 Approach: {{Bidirectional delta sync}} 
 
 
 
 Property 
 Value 
 
 
 
 
 Protocol 
 REST + {{GraphQL subscriptions / WebSocket for live}} 
 
 
 Push endpoint 
 POST /sync/push 
 
 
 Pull endpoint 
 GET /sync/pull?since={unix_ms}&entities={list} 
 
 
 Sync identifier 
 Per-entity updated_at timestamp (server clock) 
 
 
 Pull delta 
 Only records changed since last sync cursor 
 
 
 Batch size 
 Max 100 records per push, 200 per pull 
 
 
 
 
 3.2 Conflict Resolution 

 
 
 
 Entity 
 Strategy 
 Rationale 
 
 
 
 
 User profile 
 Last Write Wins (server wins) 
 Single-user edit 
 
 
 Post drafts 
 Last Write Wins (client wins on local draft) 
 User owns draft 
 
 
 Settings 
 Merge (union) 
 Non-conflicting fields 
 
 
 Counters (likes, views) 
 Server-side CRDT 
 Concurrent increments 
 
 
 {{Entity}} 
 {{LWW / CRDT / Manual / Server wins}} 
 {{Reason}} 
 
 
 
 Conflict detection: Compare updated_at + server-assigned version counter. 
 Manual conflict flow (when required): 
 
 Server returns 409 Conflict with both versions 
 App stores both versions in local DB 
 User presented with diff UI to choose version 
 Resolved version pushed back to server 
 
 
 3.3 Sync Frequency & Triggers 
 
 
 
 Trigger 
 Action 
 Conditions 
 
 
 
 
 App foreground 
 Pull sync 
 Network available 
 
 
 Mutation (create/update/delete) 
 Immediate push 
 Network available; else queue 
 
 
 AppState change: background → foreground 
 Full sync 
 > 5 min since last sync 
 
 
 Network restored 
 Drain sync queue 
 Any queued changes 
 
 
 Timer (background fetch) 
 Pull sync 
 {{Every 15 min}} 
 
 
 Push notification received 
 Pull sync for affected entity 
 Notification type = 'data_update' 
 
 
 
 
 3.4 Partial Sync / Delta Sync 
 
 Client stores last_sync_cursor per entity type (epoch milliseconds) 
 Pull requests include since cursor — server returns only changed records 
 Deleted records: server maintains soft-delete with deleted_at for 30 days 
 Client applies deletions, then removes soft-deleted records from local DB 
 
 
 4. Sync Queue Management 

 Queue storage: SQLite sync_queue table (survives app restart) 
 Queue item schema: 
 interface SyncQueueItem {
 id: string; // UUID
 entityType: string; // 'post' | 'user' | etc.
 entityId: string;
 operation: 'create' | 'update' | 'delete';
 payload: object; // Full entity data
 retryCount: number;
 maxRetries: number; // 5
 createdAt: number; // Unix ms
}
 
 Drain strategy: 
 
 On network restore: drain queue in FIFO order 
 Batch up to 50 items per push request 
 On error: retry with exponential backoff (1s, 2s, 4s, 8s, 16s) 
 After maxRetries : move to dead letter queue, notify user 
 
 TODO: Define user notification UX for sync failures. 
 
 5. Network State Detection & Handling 

 Library: {{@react-native-community/netinfo}} 
 // Network state hook
export function useNetworkState() {
 const [isOnline, setIsOnline] = useState(true);
 const [connectionType, setConnectionType] = useState<string>('unknown');

 useEffect(() => {
 return NetInfo.addEventListener((state) => {
 setIsOnline(state.isConnected && state.isInternetReachable);
 setConnectionType(state.type);
 });
 }, []);

 return { isOnline, connectionType };
}
 
 UI behavior per state: 
 
 
 
 State 
 UI Response 
 
 
 
 
 Offline 
 Banner: "You're offline — showing cached data" 
 
 
 Reconnected 
 Banner: "Back online — syncing..." (auto-dismiss 3s) 
 
 
 Slow connection 
 No extra UI (handle transparently) 
 
 
 Sync in progress 
 Subtle indicator (not blocking) 
 
 
 
 
 6. Data Flow: Online vs Offline 
 flowchart TD
 UserAction["User Action"] --> CheckNetwork{Network\nAvailable?}

 CheckNetwork -->|Yes| DirectAPI["Send to API\ndirectly"]
 DirectAPI -->|Success| UpdateLocal["Update local DB"]
 DirectAPI -->|Error| QueueAction["Queue action\n+ optimistic update"]

 CheckNetwork -->|No| QueueAction
 QueueAction --> UpdateLocal
 UpdateLocal --> UpdateUI["Update UI\n(optimistic)"]

 style UpdateUI fill:#d4edda
 style QueueAction fill:#fff3cd
 
 
 7. Testing Strategy for Offline Scenarios 

 
 
 
 Test Type 
 Scope 
 Tool 
 
 
 
 
 Unit 
 Sync queue operations 
 Jest 
 
 
 Unit 
 Conflict resolution logic 
 Jest 
 
 
 Integration 
 DB read/write with mock network 
 Jest + in-memory DB 
 
 
 E2E 
 Full offline → reconnect flow 
 Detox / Maestro 
 
 
 Manual 
 Network conditions simulator 
 Network Link Conditioner (iOS), tc (Android emulator) 
 
 
 
 E2E offline test scenario: 
 
 Open app online — verify data loads 
 Enable airplane mode 
 Perform create/update/delete actions 
 Verify optimistic UI updates 
 Verify actions queued (inspect DB) 
 Disable airplane mode 
 Verify sync queue drains 
 Verify server data matches local state 
 
 
 8. Storage Limits & Data Eviction Policy 

 
 
 
 Storage Type 
 Soft Limit 
 Hard Limit 
 Eviction Strategy 
 
 
 
 
 SQLite DB 
 50 MB 
 200 MB 
 Evict records older than 30 days 
 
 
 Image cache 
 150 MB 
 300 MB 
 LRU eviction 
 
 
 Document cache 
 200 MB 
 500 MB 
 User prompt to clear 
 
 
 Total app storage 
 500 MB 
 1 GB 
 Warn user, offer cleanup 
 
 
 
 Low storage alert: When device storage < 500 MB free, reduce cache limits by 50%. 
 User-initiated cleanup: Settings → Storage → Clear Cache option. 
 
 9. Error Handling & User Feedback 

 
 
 
 Error 
 User Feedback 
 Recovery Action 
 
 
 
 
 Sync push failed 
 Toast: "Couldn't sync — will retry" 
 Auto-retry with backoff 
 
 
 Conflict detected 
 Modal: "Update conflict — please resolve" 
 Manual resolution flow 
 
 
 Queue overflow (>500 items) 
 Warning banner 
 Partial push, user notified 
 
 
 Local DB corruption 
 Alert: "Storage error — please reinstall" 
 Offer fresh install 
 
 
 Storage limit reached 
 Alert with cleanup CTA 
 User clears cache 
 
 
 
 
 Approval 
 
 
 
 Role 
 Name 
 Date 
 Signature 
 
 
 
 
 Author 
 
 
 
 
 
 Mobile Lead 
 
 
 
 
 
 Backend Lead 
 
 
 
 
 
 Product Owner