# 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

<!-- GUIDANCE: Define WHICH features must work offline. Not everything needs offline support — be explicit. -->

| 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

<!-- GUIDANCE: Define which storage technology handles which data type. -->

### 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:**

```sql
-- 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

<!-- GUIDANCE: Define exactly HOW data flows between device and server. This is the most critical section. -->

```mermaid
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

<!-- GUIDANCE: Define the conflict strategy per data type. Not all data needs the same approach. -->

| 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):**
1. Server returns `409 Conflict` with both versions
2. App stores both versions in local DB
3. User presented with diff UI to choose version
4. 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

<!-- GUIDANCE: Define how offline actions are queued and drained. -->

**Queue storage:** SQLite `sync_queue` table (survives app restart)

**Queue item schema:**
```ts
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:**
1. On network restore: drain queue in FIFO order
2. Batch up to 50 items per push request
3. On error: retry with exponential backoff (1s, 2s, 4s, 8s, 16s)
4. After `maxRetries`: move to dead letter queue, notify user

**TODO:** Define user notification UX for sync failures.

---

## 5. Network State Detection & Handling

<!-- GUIDANCE: Define how the app detects and reacts to connectivity changes. -->

**Library:** `{{@react-native-community/netinfo}}`

```ts
// 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

```mermaid
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

<!-- GUIDANCE: Define how offline behavior is tested — unit, integration, and manual. -->

| 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:**
1. Open app online — verify data loads
2. Enable airplane mode
3. Perform create/update/delete actions
4. Verify optimistic UI updates
5. Verify actions queued (inspect DB)
6. Disable airplane mode
7. Verify sync queue drains
8. Verify server data matches local state

---

## 8. Storage Limits & Data Eviction Policy

<!-- GUIDANCE: Define maximum storage per category and what happens when limits are hit. -->

| 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

<!-- GUIDANCE: Define all error states and how users are informed. -->

| 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 | | | |