TITLE: How I Build Software That Survives Load-Shedding and Spotty Connectivity in South Africa
If you build software from a comfortable office with a gigabit fibre connection and uninterrupted grid power, your assumptions about network resilience are almost certainly broken.
Over the last two years building apps like AdminOS and VarsityOS from East London, South Africa, I’ve had to throw away standard web development playbooks. When Eskom stage 4 load-shedding hits mid-afternoon, your fancy cloud-native microservices architecture doesn't matter if the local cellphone tower drops offline ten minutes later.
Here is the exact technical stack and architectural pattern I use to build offline-first, resilient web applications that don't choke when the power goes out.
The Local-First Reality
In San Francisco, local-first is a philosophical choice about data ownership. In the Eastern Cape, local-first is survival. If a spaza shop owner or a university student in Braamfontein loses their internet connection for three hours, they cannot afford an infinite loading spinner.
Our architecture rests on three pillars:
- SQLite at the edge (via Turso or local IndexedDB adapters).
- Optimistic UI updates with zero-latency local state mutation.
- Background sync queues that retry silently when carrier signal returns.
Let’s look at how we handle local mutations without losing transactional integrity.
import { useState, useEffect } from 'turso';
interface SyncQueueItem {
id: string;
action: 'INSERT' | 'UPDATE' | 'DELETE';
table: string;
payload: Record<string, any>;
timestamp: number;
}
export function useOfflineSync(table: string) {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
flushQueue();
};
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const mutateLocally = async (payload: Record<string, any>) => {
const localId = crypto.randomUUID();
// Write immediately to local IndexedDB/SQLite cache
await window.localDb.execute(
`INSERT INTO ${table} (id, data, synced) VALUES (?, ?, 0)`,
[localId, JSON.stringify(payload)]
);
if (isOnline) {
try {
await syncToServer(localId, payload);
} catch (err) {
queueForLater(localId, payload);
}
} else {
queueForLater(localId, payload);
}
};
return { isOnline, mutateLocally };
}
async function queueForLater(id: string, payload: any) {
const existing = JSON.parse(localStorage.getItem('sync_queue') || '[]');
localStorage.setItem('sync_queue', JSON.stringify([...existing, { id, payload, timestamp: Date.now() }]));
}
async function flushQueue() {
const queue: SyncQueueItem[] = JSON.parse(localStorage.getItem('sync_queue') || '[]');
if (queue.length === 0) return;
for (const item of queue) {
try {
await fetch('/api/sync', { method: 'POST', body: JSON.stringify(item) });
// Remove successfully synced item
} catch {
break; // Stop flushing if connection drops again
}
}
}
Handling Latency Spikes (3G vs Fibre)
Most Western users are on broadband. In South Africa, a huge portion of users access web applications via mobile data on 3G or unstable 4G networks where latency can spike from 50ms to 4,000ms instantly.
If you use heavy client-side hydration frameworks without aggressive code-splitting, your JavaScript bundle size will penalize users paying high rates per megabyte. We enforce a strict rule across Mirembe Muse products: initial JavaScript bundles must stay under 85KB compressed.
We achieve this by:
- Eliminating heavy UI component libraries in favour of bespoke, headless Tailwind primitives.
- Server-side rendering critical shell layouts so the user sees painted pixels in under 300ms, even on a budget Android device.
Why This Matters Beyond Africa
When you build for extreme constraints—unstable power grids, high data costs, intermittent latency—you don't just build for Africa. You build better software.
The same resilience that keeps an inventory app running during stage 3 load-shedding will keep your app running smoothly for a user on a shaky commuter train in London or a subway in New York.
Stop designing for the ideal network condition. Build for the real world.
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support