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 Conflictwith 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_cursorper entity type (epoch milliseconds) - Pull requests include
sincecursor — server returns only changed records - Deleted records: server maintains soft-delete with
deleted_atfor 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 |
No comments to display
No comments to display