Offline Mode
A real local database in your browser, a persistent write-ahead queue, and a background worker that drains it — not just a "you're offline" banner.
The Local Database
Everything offline runs on top of a real client-side database — an IndexedDB instance (persivapps_offline) with dedicated object stores for each kind of data, so a lookup doesn't mean scanning a flat blob:
| Store | Keyed by | Holds |
|---|---|---|
smarttables | id | SmartTable metadata (name, entry/display schema versions) for offline rendering |
folders | id | Folder structure, so navigation works offline |
metadata | smarttable_id | Per-smarttable settings needed to render the entry form without a server round-trip |
columns | smarttable_id | Column/field definitions for the entries table view |
entries | id, indexed by smarttable_id and smarttable_data_entry_id | Cached entry data — both what's synced from the server and what you've created offline |
offline_queue | auto-increment id, indexed by temp_id and status | Every write you've made offline, waiting to be sent — see below |
sync_log | auto-increment id | A record of every completed sync run, for audit/troubleshooting |
The schema is versioned (currently version 10) with real migration logic — indexes get rebuilt and stale stores get dropped automatically the first time a new app version opens an older browser's database, rather than requiring you to clear site data.
What Works Offline vs. What Doesn't
- Viewing smarttables, folders, and previously loaded entries — served from the stores above, not the network
- Creating and editing entries — written to
entriesimmediately and queued for sync (see below) - Autofill suggestions, using the last-synced reference data
- Past-record suggestions from your own previously entered data
The Offline Queue — how a write actually gets there
Saving or updating an entry offline doesn't fail and doesn't silently disappear — it's written as a queue item in offline_queue with a real state machine: pending → syncing → done, with failed and cancelled as the other two terminal-ish states. Each item records its type (SAVE_ENTRY or UPDATE_ENTRY), its payload, a timestamp, and — critically — a temp_id.
temp_id matters: chained edits
A brand-new offline entry doesn't have a real server ID yet, so it's assigned a temporary one (prefixed offline_) until sync confirms it. If you create an entry offline and then edit it again before ever syncing, both the create and the edit share that same temp_id — they're a chain. Cancel the first one and the whole chain cancels together, rather than leaving an edit queued against a create that will never happen. Once the server confirms the create and hands back a real ID, every other queued item referencing that temp_id — including its URL and any embedded entry-id reference in the payload — is rewritten to the real ID automatically, and the local entries row is migrated from the temp key to the real one.
| Indicator | Meaning |
|---|---|
| Offline banner | Shown when the app detects no connection; includes a "Sync Now" button |
| Sync queue badge | Live count of items in pending or failed state |
| Sync status indicator | Whether a sync is currently running, succeeded, or failed |
How Sync Actually Runs — a Web Worker, not the main thread
Sync doesn't run on the same thread as the UI. On first use, the app spins up a dedicated Web Worker (js/offline-worker.js) and hands it your API base URL and access token; from then on, syncing, connectivity checks, and speed measurement all happen off-thread so a large sync doesn't freeze typing or scrolling. If the browser can't create a Worker for some reason (some embedded WebView contexts can't), the same logic runs a fallback pass on the main thread instead — you get the same behavior, just without the isolation.
- As soon as the browser's
onlineevent fires, sync starts automatically - You can also trigger it manually with Sync Now
- Every code path that can trigger a sync — the automatic online listener, the manual button, the Sync Log modal, even the network-speed monitor — checks a single
offlineModeEnabledflag at the point of sending the message to the worker, rather than trusting every caller to check it first. This was a deliberate fix: gating it only at initialization reliably missed some of these call sites in practice. - A
sync_logentry records what was synced and when, so you can confirm nothing was lost after an extended offline stretch
done simply stays pending/failed and is picked up on the next successful connection. There's no in-memory-only sync state that a crash could lose.
Network Speed Indicator
The same Web Worker measures actual throughput in the background (polling roughly every 15 seconds) and reports it back to the UI as a 0–4 bar indicator, so you know whether to expect slower saves and syncs before you start a long entry — this isn't just navigator.onLine (which only tells you a network interface exists, not that it's usable).
Service Worker & App-Shell Caching
Separately from the IndexedDB layer above, a browser Service Worker caches the app's own code (JS/CSS/HTML) so the app can even launch with no connection, not just keep working once it's open. The offline-sync worker script and its dependencies are explicitly precached on install — before the general caching strategy takes over — specifically so the offline-save path is guaranteed to be available from the very first load rather than picked up opportunistically on a later visit.
Mobile App
PersivApps is also available as a native mobile app for Android and iOS — a lightweight wrapper around the same app described throughout this site, not a separate product to relearn. It adds a few things a browser tab can't do on its own:
- Native print & share — printing and sharing a document use your device's native print dialog and share sheet instead of a browser-only download
- Downloads — files save straight to your device's Downloads folder, with a completion notification
- Push notifications — the same notifications described on the Notifications page, delivered as native mobile push
- Faster reloads while offline — the app keeps its own local copy of the app shell, so it can reopen even from a cold start with no connection