Skip to main content
Jabal Al Noor Pharmacy Digital Commerce Platform
Case StudyMobile Dev

A delivery app for a basement with no signal

An offline-first PWA with two persistence layers, and error handling that refuses to treat a lost connection like a server rejection.

Client
Jabal Al Noor Pharmacy
Industry
Pharmacy retail
Outcome
Full offline task flow

The problem

Delivery agents lose connectivity in lifts, car parks and stairwells — mid-task, holding a phone and a customer medicine. An app that fails a status update in that moment is worse than paper.

Two persistence layers

A Next.js PWA with two stores, chosen for different jobs:

LayerStoreWhy
ReadsTanStack Query to localStorage, 24h max ageA cold offline start still renders the assignment list
WritesIndexedDB via DexieWrites include photos, and localStorage is string-only

Offline, an agent can mark picked up, failed, and delivered-with-photo, and the UI updates immediately.

The error handling is the design

On failure the app branches on why:

  • Network error — the optimistic state is deliberately kept and the mutation is queued
  • Any other error — roll back

A server rejection and a lost connection are not the same event and must not be treated alike.

One storage detail worth knowing

Captured photos are stored as a raw ArrayBuffer rather than a Blob, because Blobs do not survive IndexedDB structured-clone in every environment — including jsdom, which would have made this untestable. A File is reconstructed at drain time.

Draining the outbox

On reconnect the queue drains oldest-first with three-way triage:

  1. Conflict (400/403/404 — reassigned, already terminal, invalid) — dropped permanently and surfaced to the UI, because it can never succeed on replay
  2. Network error — increment attempts and break, since we are still offline; resume on the next online event
  3. Anything else (transient 5xx) — increment attempts and continue to the next entry

Auto-retry stops at five attempts per entry, leaving it for a manual tap-to-retry so a wedged request cannot loop forever.

What is explicitly out of scope

The primary trigger is the browser online event rather than the Background Sync API. That is a deliberate accommodation: iOS Safari PWAs do not support Background Sync, so the service-worker sync message is a best-effort secondary on Chrome and Android only.

The design doc lists true cross-platform background sync guarantees as out of scope rather than pretending otherwise.

A delivery app for a basement with no signal | Tuninbits