<PS/>
Article

Building Software for Places Where the Internet Doesn't Work

Badhan's blood donor platform for Amar Ekushey Hall needed to work in hospital basements, rural clinics, and emergency wards with no connectivity. Offline-first isn't a feature — it's an architecture decision made early.
PSParvej Shah
January 2, 2026· Last updated: August 26, 20264 min read
Offline-First PWA Architecture Cover

Blood donor matching is time-sensitive in a way most software problems aren't. When a patient needs a specific blood type during a critical procedure, the medical team is working with a narrow window. The volunteer coordinator needs to identify available donors, contact them, and arrange a donation quickly.

Badhan is the blood donation organization of the Amar Ekushey Hall unit at the University of Dhaka, operating a volunteer donor network for the Dhaka Medical College Hospital. The donor management platform needed to be fast, usable by volunteers with varying technical experience, and — critically — functional in hospital environments where network connectivity is unreliable.

Anyone who has been inside a large hospital building knows the problem: thick concrete walls, basement floors, and dense building infrastructure create mobile dead zones. A web application that requires network connectivity to display a list of blood donors is simply not useful in these environments.

Offline-First vs. Offline-Capable

There's an important distinction between applications that are offline-capable and applications that are offline-first.

An offline-capable application handles the absence of network connectivity gracefully — it shows a cached version of content it previously loaded, or displays a "you're offline" message without crashing. This is the baseline minimum.

An offline-first application treats local storage as the primary data source. All reads come from local storage first. Network requests are used to synchronize local data with the server, not to serve the request. The application is fully functional without a network connection, not just tolerable.

For Badhan's use case, offline-capable was insufficient. If a volunteer could only search the donor directory online, the application would fail exactly when it was needed most.

The Data Synchronization Architecture

All donor records, blood group data, and volunteer contact information are stored in the browser's IndexedDB. When the application loads with a network connection, it syncs any changes from the server to local IndexedDB. When the volunteer searches for donors, the query runs against local IndexedDB with zero network involvement.

interface LocalDonorRecord {
  id: string;
  name: string;
  bloodGroup: "A+" | "A-" | "B+" | "B-" | "AB+" | "AB-" | "O+" | "O-";
  contactNumber: string;
  lastDonationDate: Date | null;
  isEligible: boolean;   // pre-computed flag
  hallName: string;
  roomNumber: string;
  lastSyncedAt: Date;
}

async function syncDonorRecords(): Promise<void> {
  const lastSync = await getLastSyncTimestamp();
  
  const updates = await fetch(`/api/donors?updatedSince=${lastSync.toISOString()}`)
    .then(r => r.json());

  const db = await openLocalDB();
  const tx = db.transaction("donors", "readwrite");

  for (const donor of updates) {
    const isEligible = donor.lastDonationDate
      ? daysSince(donor.lastDonationDate) >= 90
      : true;

    await tx.store.put({ ...donor, isEligible, lastSyncedAt: new Date() });
  }

  await tx.done;
  await setLastSyncTimestamp(new Date());
}

We pre-compute the isEligible boolean flag on dataset sync. Blood donation guidelines require a minimum 90-day gap between donations. Pre-computing it means the calculation happens once at sync time, not on every search query, enabling instant filtering in the critical search flow.

Service Worker Caching Strategy

Workbox handles the service worker layer, managing pre-caching for static assets and runtime caching strategies for API responses.

import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { NetworkFirst } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";

precacheAndRoute(self.__WB_MANIFEST);

registerRoute(
  ({ url }) => url.pathname.startsWith("/api/donors"),
  new NetworkFirst({
    cacheName: "donor-api-cache",
    networkTimeoutSeconds: 4,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 24 * 60 * 60,
      }),
    ],
  })
);

The NetworkFirst strategy with a 4-second timeout means: try the network first. If the network responds within 4 seconds, use that response and update the cache. If not, serve the cached response. This gives volunteers fresh data when the network is marginal but usable, and cached data when it's completely unavailable.

The Offline-First Side Effect: Speed

The most practical aspect of the offline-first approach turned out to be speed rather than connectivity. IndexedDB queries for blood group filtering across hundreds of records return in under 10 milliseconds consistently. This is faster than any network request and faster than most server-side database queries when you account for round-trip time.

The offline-first architecture that we built for connectivity resilience also produced a noticeably snappier search experience under normal conditions.

That's usually how it works: designing for the hard constraint improves performance under the easy conditions too.

Enjoyed the read?

Have a product idea worth building — let's talk.

Start a project