DEV Community

sharkflow ltd
sharkflow ltd

Posted on

FlowTasks — devto

{
  "title": "Building FlowTasks for Africa's Mobile-First Entrepreneurs: The Engineering Behind Productivity at Scale",
  "content": "# Building FlowTasks for Africa's Mobile-First Entrepreneurs: The Engineering Behind Productivity at Scale\n\nWhen we started building FlowTasks at SharkFlow, we weren't thinking about Silicon Valley's productivity stack. We were thinking about a Nairobi-based logistics entrepreneur managing 50+ daily deliveries on a 3G connection, or a Kampala fashion business owner juggling inventory and client orders during peak hours.\n\nThat constraint — unreliable connectivity, limited bandwidth, low-end devices — became our north star for technical design. Here's how we built productivity software that actually works in Africa.\n\n## The API Philosophy: Offline-First, Sync-Later\n\nMost task management tools assume constant connectivity. We built the opposite.\n\n```

typescript\n// Core sync strategy: works offline, syncs when possible\ninterface Task {\n  id: string;\n  title: string;\n  status: 'pending' | 'in-progress' | 'completed';\n  priority: number;\n  createdAt: number;\n  updatedAt: number;\n  syncStatus: 'local' | 'syncing' | 'synced' | 'conflict';\n  localVersion: number;\n  serverVersion?: number;\n}\n\nclass TaskService {\n  async addTask(task: Omit<Task, 'id' | 'syncStatus'>): Promise<Task> {\n    // 1. Write to local storage immediately\n    const localTask = {\n      ...task,\n      id: generateUUID(),\n      syncStatus: 'local',\n      localVersion: 1\n    };\n    await this.localDB.insert('tasks', localTask);\n    \n    // 2. Queue for sync (doesn't block the user)\n    this.syncQueue.enqueue(localTask);\n    \n    // 3. Return immediately to UI\n    return localTask;\n  }\n}\n

```\n\nThis pattern is critical. On African networks, you can't afford to wait for API responses before letting users see their work. We use SQLite on mobile and IndexedDB on web — local-first databases that exist entirely on the device.\n\n## Database Strategy: SQLite + EdgeDB + Smart Replication\n\nOur stack is deliberately minimal:\n\n- **Mobile**: SQLite (battle-tested, 600KB, zero dependencies)\n- **Edge**: EdgeDB (typed queries, built-in migrations, excellent JSON support)\n- **Sync logic**: Custom conflict-resolution engine\n\nWhy not PostgreSQL directly? Three reasons:\n\n1. **Network unpredictability**: A dropped connection mid-transaction on spotty 3G is painful with traditional SQL. Our replication layer handles partial syncs.\n2. **Schema versioning**: With 60M+ mobile money users spreading across Africa, we needed schema evolution without API versioning hell.\n3. **Real-time collaboration**: EdgeDB's subscription API gives us WebSocket-based sync for free, critical when three team members are updating tasks simultaneously.\n\n```

typescript\n// Conflict resolution: last-write-wins with tombstones\nclass SyncEngine {\n  async resolveConflict(\n    local: Task,\n    server: Task\n  ): Promise<Task> {\n    // If both changed, compare timestamps\n    if (local.updatedAt > server.updatedAt) {\n      return local; // Trust local if more recent\n    }\n    \n    // For critical fields (like payment status), require manual merge\n    if (this.isCriticalField(local, server)) {\n      return { ...server, syncStatus: 'conflict' };\n    }\n    \n    return server;\n  }\n  \n  async bulkSync(localTasks: Task[]): Promise<SyncResult> {\n    // Batch uploads to minimize API calls\n    const chunks = chunk(localTasks, 50);\n    const results = await Promise.all(\n      chunks.map(c => this.api.batchUpsert('/tasks', c))\n    );\n    \n    return this.mergeSyncResults(results);\n  }\n}\n

```\n\n## Bandwidth Optimization: Why We Cache Aggressively\n\nM-Pesa's $320B annual transaction volume runs on 2G. Your task app needs to respect that.\n\n```

typescript\n// Request batching + compression\nclass NetworkOptimizer {\n  private requestBatch: any[] = [];\n  private batchTimeout: NodeJS.Timeout;\n  \n  async queueRequest(endpoint: string, data: any) {\n    this.requestBatch.push({ endpoint, data });\n    \n    // Wait 2 seconds for more requests to batch\n    clearTimeout(this.batchTimeout);\n    this.batchTimeout = setTimeout(() => this.flushBatch(), 2000);\n  }\n  \n  private async flushBatch() {\n    if (!this.requestBatch.length) return;\n    \n    const payload = JSON.stringify(this.requestBatch);\n    const compressed = await gzip(payload);\n    \n    // Typical reduction: 40KB → 8KB\n    await fetch('/api/batch', {\n      method: 'POST',\n      headers: { 'Content-Encoding': 'gzip' },\n      body: compressed\n    });\n    \n    this.requestBatch = [];\n  }\n}\n

```\n\nWe compress all payloads by default. On a typical task sync (50 tasks), this drops from 40KB to 8KB. Over a month, that's 1GB saved for users in high-cost data markets.\n\n## Smart Scheduling: Syncing When It's Free\n\nHere's something most productivity apps miss: in Kenya, Uganda, Tanzania, many plans offer free data during specific hours. We detect and exploit that.\n\n```

typescript\n// Intelligent sync scheduler\nclass SyncScheduler {\
Enter fullscreen mode Exit fullscreen mode

Top comments (0)