All writing
6 min read

Making a Quran PWA That Works Offline

Building an offline-first Quran app meant a service worker that survives no connection, on-device prayer times for 30+ countries, and a couple of service-worker bugs that taught me humility.

PWAOfflineService Workers

I built a Quran PWA — a reader, a prayer-time calculator, Azkar, a Masbaha counter, and a daily verse and hadith — with one non-negotiable: it has to work with no connection at all. People reach for it during prayer, on a commute, in places with patchy signal. Offline is not a nice-to-have here; it is the product.

Offline-first, not offline-maybe

The whole thing is vanilla JavaScript with a service worker that caches the app shell on install. The first visit primes the cache; every visit after that the app boots from cache and only touches the network for things it genuinely does not have yet.

js
const SHELL = 'quran-shell-v1';
const ASSETS = ['/', '/index.html', '/app.css', '/app.js'];

self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(SHELL).then((c) => c.addAll(ASSETS)));
});

self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then((hit) => hit || fetch(e.request)),
  );
});

Cache-first for the shell means instant loads and a UI that never blanks out just because the network did.

Prayer times without a server

Prayer times are calculated on the device from the user's coordinates, so they work offline too. With location permission the app covers 30+ countries' calculation conventions, and a magnetic-compass view points to the qibla. Everything the user picks — location, fonts, theme — persists in localStorage, so the app remembers them with no account and no backend.

The bug that taught me humility

Service workers are wonderful until they are not. My worst bug was an update-toast loop: I showed a 'new version available' prompt when a new worker was waiting, but my detection fired on every navigation, so users got the toast on a loop. The fix was to listen for the waiting worker properly, prompt once, and reload only when control actually changes.

js
navigator.serviceWorker.addEventListener('controllerchange', () => {
  window.location.reload(); // the new worker took over — refresh once
});

I also learned to version the cache name and delete old caches on activate, or yesterday's assets quietly stick around forever.

The lesson

Offline-first changes how you think. You stop assuming the network is there and start treating it as an enhancement. The reward is an app that feels instant and keeps working when everything else on the phone has given up — which, for something people lean on every day, is the entire point.


Written by Fady Ehab Amer

Get in touch →

Keep reading