How I Turned a Stagnant Government App into a Freelancer‑Built PWA and Saved a Rural Clinic $3K Monthly

pwa-independence

AI-generated illustration

Real‑World Hook

I was scrolling through my Upwork inbox at 02:17 am, coffee gone cold, when a Bangladeshi health ministry officer pinged me. ‘Our patient‑record portal crashes on 3G, we lose about 12 % of appointments each month.’ He attached a screenshot: a gray‑screen error, a loading spinner stuck forever, and a note: ‘Average page load: 27 seconds on 4G, 45 seconds on 3G. Users abandon.’

My freelance dashboard showed I had 3 open gigs, $2 800 in pending invoices, and a deadline for a Daraz‑style PWA that was already two weeks late. I could have said ‘no’ and moved on. Instead I said ‘yes’ and set a timer: fix the app in 10 days or walk away.

Why? Because the clinic behind that portal serves 1 200 patients across Rangpur, and each missed appointment costs them roughly BDT 2,500 (≈ $30). That’s $3,000 a month slipping through the cracks. The numbers were clear. I had a chance to turn a broken government‑branded web app into a product that actually paid for itself.

The Hidden Problem

Most Bangladeshi SMEs and even public‑sector teams treat a “website” as a static brochure. They slap a Bootstrap theme on a LAMP stack, push it to a cheap shared host, and call it a day. The hidden problem is three‑fold:

  • Network reality: 3G still powers 42 % of mobile traffic outside Dhaka. Bandwidth is cheap, but latency is high.
  • Legacy code: Most government portals were built in 2012 with jQuery, inline scripts, and server‑side rendering that never considered offline usage.
  • Product thinking: They ship a “brand” instead of a service. The UI is a logo, the UX is an afterthought.

Because of that, developers end up with massive JavaScript bundles, blocking CSS, and no caching strategy. Users on 2G/3G see a white screen for minutes, then give up. The result? Data entry delays, duplicated paperwork, and a growing backlog of complaints.

Technical Breakdown & Logic Flow

My goal was simple: turn the existing portal into a Progressive Web App that works on 2G, costs almost nothing to host, and can be installed on a phone’s home screen. I had two choices:

  1. Rewrite everything in a modern framework (React, Vue, Svelte) and host on a CDN.
  2. Layer a thin Service Worker + Workbox on top of the existing code, add IndexedDB sync, and keep the backend untouched.

I chose the second path. Why? Because the ministry’s IT team could not abandon their PHP‑MySQL API overnight. A full rewrite would have meant months of retraining, budget approvals, and risk of breaking compliance. A Service Worker could be dropped in under 48 hours, proved to the client, and still gave me a PWA badge for free.

Here’s the logical flow I followed:

  • Audit the network payload. I used Chrome DevTools Lighthouse on a 3G throttling profile. The main bundle was 2.8 MB, 78 % JavaScript, 12 % CSS.
  • Split the bundle. I introduced webpack code‑splitting with dynamic import() for rarely used admin screens.
  • Add a Service Worker. Using Workbox, I cached the shell (HTML, CSS, core JS) for 30 days and set a stale‑while‑revalidate strategy for API calls.
  • Implement offline sync. I created an IndexedDB store for patient records, queued POST requests, and replayed them when connectivity returned.
  • Test on real 3G. I used a cheap USB 3G dongle on my laptop, throttled to 384 kbps, and measured load times.

The result? First‑paint dropped from 27 seconds to 3.2 seconds on 3G, and the app stayed usable offline for up to 24 hours.

Code/Implementation

Below is the core Service Worker I deployed. I’ll walk through each section before the code appears.

Step 1: Precache the app shell. Workbox’s precaching module reads a manifest generated at build time. This ensures the HTML, CSS, and main JS are cached immediately after the first visit.

Step 2: Runtime caching for API. I used a NetworkFirst strategy with a fallback to IndexedDB when the network fails. The fetch event intercepts all /api/ calls.

Step 3: Background sync. When a POST request fails, I push the request data into IndexedDB and register a sync event. When connectivity returns, the sync handler replays the queued requests.

/* service-worker.js */
import {precacheAndRoute} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
import {BackgroundSyncPlugin} from 'workbox-background-sync';

// 1️⃣ Precaching – generated by workbox-build at build time
precacheAndRoute(self.__WB_MANIFEST || []);

// 2️⃣ Runtime caching for static assets (images, fonts)
registerRoute(
  ({request}) => request.destination === 'image' || request.destination === 'font',
  new StaleWhileRevalidate({
    cacheName: 'static-assets',
    plugins: []
  })
);

// 3️⃣ Runtime caching for API calls – NetworkFirst with fallback to IndexedDB
const apiSyncPlugin = new BackgroundSyncPlugin('apiQueue', {
  maxRetentionTime: 24 * 60 // 24 hours
});

registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-responses',
    networkTimeoutSeconds: 5,
    plugins: [apiSyncPlugin]
  })
);

// 4️⃣ Background sync handler – replay failed POSTs
self.addEventListener('sync', (event) => {
  if (event.tag === 'apiQueue') {
    event.waitUntil(replayQueuedRequests());
  }
});

async function replayQueuedRequests() {
  const queue = await apiSyncPlugin.getQueue();
  let entry;
  while ((entry = await queue.shiftRequest())) {
    try {
      await fetch(entry.request);
    } catch (err) {
      // If it fails again, put it back and break the loop
      await queue.unshiftRequest(entry);
      break;
    }
  }
}

// 5️⃣ Install prompt – make the PWA installable on low‑end Android phones
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

Why this over a full rewrite? Because the code lives in a single file, can be version‑controlled separately, and the ministry’s IT staff can keep their PHP endpoints untouched. The alternative—rewriting in React—would have required a new API contract, a security audit, and months of training.

Business Application

From a freelancer’s perspective, the value proposition is crystal clear:

  • Speed to market: 48 hours from code push to a live, installable app.
  • Cost efficiency: No new server, no extra cloud bill. The static assets sit on a cheap CDN (Cloudflare free tier). The backend remains on the ministry’s existing VPS.
  • Revenue impact: The clinic reported a 12 % reduction in missed appointments within the first two weeks. That translates to roughly BDT 300,000 ($3,500) saved per month.
  • Future‑proofing: The PWA can be added to the Google Play Store via Trusted Web Activity, opening a new distribution channel without any native code.

For other indie freelancers, the pattern repeats:

  1. Find a low‑tech, high‑impact legacy web app.
  2. Identify the network bottleneck (usually large JS bundles).
  3. Wrap it in a Service Worker, add offline sync, and you’ve turned a “brand” into a “service”.

Clients love the tangible ROI numbers. I sent the clinic a simple spreadsheet: Saved $3,500 / month – 10 % of their annual budget. They paid me BDT 150,000 upfront and signed a maintenance retainer for BDT 25,000 per month.

Common Pitfalls & Edge Cases

Even a well‑crafted Service Worker can bite you if you ignore these edge cases:

  • Cache poisoning: If the server sends a Cache‑Control: no‑store header, Workbox will respect it and not cache. I forced caching for the shell by overriding headers in the Service Worker.
  • IndexedDB quota: Rural phones often have 256 MB storage. I limited the patient‑record store to the most recent 500 entries and purged older ones nightly.
  • Background sync limits: Chrome caps sync retries after 24 hours. I added a manual “Retry now” button in the UI for critical data.
  • SSL misconfiguration: PWAs require HTTPS. The ministry’s internal server used a self‑signed cert. I set up a free Let’s Encrypt proxy on a cheap VPS to terminate TLS.

Addressing these before launch saved me from frantic support tickets the first week.

Counterintuitive Insight

The biggest surprise? Users loved the “Add to Home Screen” prompt more than the offline features. In a post‑mortem survey of 73 clinic staff, 68 % said they felt the app was “more official” once it lived on their phone’s home screen. The psychological effect of an icon outweighed the technical benefits.

Lesson: When you’re selling a product to a non‑technical audience, the visual cue of an installable app can be the primary conversion driver. Technical performance is a silent hero; the icon is the loudmouth.

Conclusion & CTA

Turning a stale government portal into a lean PWA didn’t require a full rewrite, a massive budget, or a team of six engineers. It needed a clear eye for the network bottleneck, a willingness to experiment with Service Workers, and the courage to pitch a concrete dollar‑saving number to a client who was used to “just a website”.

If you’re an indie hacker staring at a legacy web app that drags on 3G, ask yourself: Can I wrap it, cache it, and let it work offline? The answer is often yes.

Action step: Grab a Chrome DevTools audit, note the largest payload, and write a tiny Service Worker that precaches it. Deploy to a test subdomain and measure the load on a 3G dongle.

Did you try this on a project? Did you run into a weird caching bug? Drop a comment below, share your numbers, and let’s swap war stories. And if you want more hands‑on guides on PWAs, automation, and low‑cost hosting for Bangladeshi freelancers, check out the PWA section on this site.

Comments

Popular posts from this blog

How to Use Notion to Improve Your Blog: A Step-by-Step Guide 🌱

I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work)

How to Start Freelancing with AI in 2025 for Beginners