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
The dividing line is simple: anything answerable from data already on the device works; anything needing the server to decide something does not.
| Action | Offline | Why |
|---|---|---|
| View smarttables, folders and previously loaded entries | Works | Served from the local stores, not the network. Every SmartTable you opened while online is stored in full (all pages, refreshed at most every 10 minutes), so search and filters offline see every row. |
| Create and edit entries | Works | Written to entries immediately and queued for sync. |
| Autofill suggestions | Works | Uses the last-synced reference data, so it can be stale. |
| Past-record suggestions | Works | Comes from your own previously entered data, held locally. |
| Submitting for approval | Needs a connection | Validation happens server-side; the decision cannot be made on the device. |
| PDF / print output | Needs a connection | Rendered server-side. |
| Large file uploads | Needs a connection | Past the size the local queue will hold. |
| Anything depending on another user's live state | Needs a connection | For example, whether someone else currently holds an edit lock on the same entry — only the server knows. |
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 |
When a Change Won't Sync — "Needs attention"
Most failed uploads (no signal, a server hiccup) are retried automatically with a growing delay. Two kinds are not: a change the server rejected (for example a required field is missing, or you no longer have access), and a change that failed 25 times. These show in the Sync Log's Pending Queue as Needs attention, with the server's reason underneath, and Sync Now skips them, since sending the same thing again would fail the same way. A red note at the top of the queue says how many there are.
Each queued change has three buttons:
| Button | What it does |
|---|---|
| Retry (only on "Needs attention") | Puts the change back in the queue and syncs straight away. Use it once the cause is fixed, e.g. after an admin restores your access. |
| Download (arrow icon) | Saves one .json file with the exact request and every attached photo or file inside it. Send this file to your support contact, and they can create or update the entry for you. It does not contain your password or login token. |
| Discard | Deletes the change from this device for good. Download it first if you may need it. |
The file is named offline-request_<type>_st<SmartTable id>_q<queue id>_<YYYYMMDD_HHMMSS>.json, with the time in UTC.
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