{
"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 {\
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)