Offline-first portals — why your parent app must work on 2G
We did a survey. 1 200 parents across 18 schools. The question was simple: "Where do you open the school app most often?"
The top three answers, in order:
- Metro platform / inside the metro coach (32%)
- Basement / underground parking at office (24%)
- At home, on patchy Wi-Fi (19%)
Only 14% said "at home on stable Wi-Fi". 11% said "at the school gate".
That's the architecture brief. If your parent app needs five bars of LTE to be useful, half your users hate you and don't tell you why.
What "offline-first" actually means
It's not "show a stale spinner forever". It's a four-part contract:
- The shell loads from local cache. The app opens in under a second even with no network.
- Recently-viewed data is locally available. Last seen attendance, last seen fee balance, last seen pickup ETA — all readable when offline.
- Writes queue locally. A leave request submitted on a metro platform doesn't fail; it queues, the UI shows "pending sync", and it submits when the network is back.
- Sync is idempotent. When the queue flushes, every submission carries a
ClientRequestIdso a re-sync never double-creates a leave request.
This is harder than it sounds. The naïve "cache last response" gets you 60% there and breaks the other 40%.
How we built it
IndexedDB-backed cache
We use IndexedDB (via a Blazor-friendly wrapper) for two stores:
- Reads cache — last response from each API call, keyed by URL + auth context.
- Writes queue — pending submissions waiting for network.
Reads are served from cache first, then refreshed from network in background. The UI gets a "data is stale (last refreshed 12 min ago)" hint when the cache is older than threshold.
Idempotent submission
Every form that creates something on the server generates a UUID client-side. That UUID is the ClientRequestId. The server treats (TenantId, UserId, ClientRequestId) as a uniqueness key. Re-submitting the same logical request returns the same row, not a duplicate.
We added this to leave requests, fee payment confirmations, and attendance corrections so far. Roll-out to the rest is happening per-feature.
"Pending sync" UI
A small badge in the navigation shell shows the count of pending writes. Tapping it opens a list — each row says "Leave request for Class 8 · pending · last attempt 2 min ago · will retry on next connection". The user knows what's queued; nothing is invisible.
Service-worker shell
The app shell HTML + CSS + JS is cached by a service worker on first load. Subsequent opens are essentially instant. We update the shell when a new version is deployed via standard SW lifecycle.
What 2G actually means
Most "offline-first" articles assume "0 bars vs 5 bars". The reality on Indian 2G is 5 KB/s with 4-second packet delays. The app works but every request feels broken because the spinner spins for 10 seconds.
That's the case we optimised for. Specifically:
- Reads return cached data instantly, refresh in background. No spinner.
- Writes show optimistic UI. "Leave request submitted" appears immediately; the actual server round-trip happens in the background, and if it fails the UI rolls back with a clear message.
- Big payloads downscale on client. Document uploads to admission forms get downscaled in the browser to ~500 KB before upload.
What we got wrong
Three things we shipped, then walked back:
- Background sync timer. Originally we tried to sync every 5 minutes when the network was up. Burned battery. Replaced with "sync when the user opens the app and on the connection-restored event".
- Sync indicator on the home screen. Too prominent — parents thought something was wrong. Moved to the nav drawer.
- "Last refreshed" timestamps on everything. Too noisy. Now only shown when the cache is older than 24 hours or when the user explicitly pulls down.
When offline-first doesn't apply
A few flows must be online:
- Online fee payment — requires the gateway round-trip, can't be cached.
- Live class join — needs a real-time link.
- AI quiz generation — provider call, no cache.
For these, the UI shows a clear "needs internet" state and explains why.
If your school is shopping for a parent app, the test is: put a phone in airplane mode, open the app, scroll around for 30 seconds, then turn airplane mode off. If the app worked the whole time and submitted everything you did, it's offline-first. If not, your parents are about to be unhappy.
Request a demo — try our parent app on airplane mode.