Make offline work recoverable

Separate caches from user data, manage browser lifecycles and test storage failure and recovery.

Part 3 of 4 · Published 16 October 2026

A field-report application should let someone finish an observation when the connection disappears. Opening downloaded material, searching it and saving a draft can use the device already in their hands. Rails still owns authorization, shared records and the server database. The proposed offline path commits locally, reports that state honestly and synchronizes when the server is reachable.

Three distinct promises

Do not display success when a write is merely queued. With IndexedDB, wait for transaction completion rather than one request’s success. That completion is still not a guarantee against every hardware failure or browser cleanup. MDN’s IndexedDB guide explains transaction and shutdown behavior.

“Three reports waiting to sync” tells someone to retain the device and export before clearing browser data. “All reports synced” refers to acknowledged versions; an authorized second device still needs to fetch them. A later local edit returns the relevant report to a pending state.

Separate application files from user records

Use Cache Storage for the resources needed to open the application; use a database for reports. A cached HTML response is neither an editable record store nor an outbox. Our proposed policies distinguish routes:

These are choices to validate, not one universal strategy. MDN’s caching guide explains the patterns. Never make a personalized server-rendered page the universal fallback for the next person using the browser.

The first successful visit matters. The browser cannot open an uncached application while offline. Display “Available offline” only after required assets and database initialization succeed. Give attachments their own downloaded/available states.

Use IndexedDB first

For the first version, choose IndexedDB: asynchronous structured storage and transactions without another database engine. Proposed stores are reports, outbox and metadata. Each report has a client-generated UUID, account scope, body, local revision, last acknowledged server version and deletion state. The outbox holds durable edit intentions; when an intention becomes a sendable operation, fix its UUID, report ID, proposed change and base server version. An operation’s identity and content remain immutable across retries. Later edits can remain staged behind an in-flight predecessor, as Part 4 explains. Metadata tracks schema version and the synchronization cursor.

Write the edit and its queued intention in one transaction spanning both stores. Both commit or neither does. Otherwise a crash could leave a saved-looking report with no pending intention, or an intention that does not match the visible report.

Prepare data before opening the transaction and keep it short. Network work belongs outside it. On schema changes, old tabs can block an upgrade by retaining database connections. Handle version-change notifications and explain when tabs must close or refresh rather than leaving a spinner forever. The IndexedDB guide linked above covers these mechanics.

OPFS and SQLite add capabilities and costs

The Origin Private File System is useful for file-oriented workloads. It is private to an origin, not a normal user-visible backup folder. Synchronous access handles belong in dedicated workers; broader OPFS APIs include asynchronous operations.

SQLite compiled to Wasm can be justified by existing SQL, relational queries or dataset size. It is not required because Rails uses SQL. Its browser adapter adds worker setup, locking, multi-tab behavior and import/export decisions. SQLite’s persistence documentation describes VFS choices with different concurrency and isolation requirements. Some require cross-origin isolation headers; the access-handle pool has different constraints. Generic OPFS availability does not establish compatibility for every configuration.

IndexedDB is sufficient for this proposed report application. An attachment-heavy edition could add OPFS. Independent record and file stores have no shared transaction: write and verify the file before committing its reference, retain recovery metadata, and safely collect unreferenced files. Never mark an attachment ready before its bytes exist.

Storage management is a product feature

Browser storage has quotas, deletion and eviction policies. navigator.storage.estimate() reports estimates; navigator.storage.persist() requests stronger protection against automatic eviction. Neither creates a backup or guarantees every write. See MDN’s storage policy overview.

Provide a panel showing estimated use, pending operations, downloaded attachments and the latest synchronization. Separate removing replaceable downloaded copies from deleting user records. Do not silently discard pending edits to make room. If a write fails, retain the editor’s text, mark it unsaved and offer copying or download.

An export should include a versioned manifest, records, pending operations and promised attachments, with their states clearly marked. Import validates format and account scope, preserves existing work and reports conflicts. Test a round trip in a clean profile. A generated download is useful only if the person retains a readable copy.

Make persistence outcomes visible

The Storage Standard starts local storage in best-effort mode. Request persistence from the window: persist() is not exposed to a Ruby worker, although estimate() and persisted() can be. Show whether protection was granted, remains best-effort, is unsupported or failed. A grant still does not guarantee a write or provide a backup. Application caches and record stores are logical divisions, not independent protection against bucket deletion; the shell and pending outbox can both disappear.

Proposed checks, not executed here: in a disposable origin, exercise unsupported, false, true and rejected persistence requests, then verify the actual write/read/reload path and recovery from a retained export or server copy. Clearing that fixture simulates storage loss; it is not evidence that real storage-pressure eviction was tested. Keep quota-error handling after a grant, and preserve unsaved editor text.

Updates must preserve yesterday’s work

The Service Workers specification permits termination; memory must be disposable. Prepare the complete new asset set before activation. Avoid mechanically pairing skipWaiting() and clients.claim() with incompatible page code or schema changes: an open tab may still run an earlier release.

Prefer an “Update ready” prompt after local edits commit, and preserve compatible migrations and API behavior during transition. Service-worker activation and database migration are separate events. MDN’s lifecycle guide explains the update mechanics. Test interrupted upgrades and old/new tabs together. Deleting the database must not be the standard repair procedure.

Installation is optional; failure is explicit

Offline functionality should work in a supported normal browser tab. Installation can improve access and presentation, but requirements and UI vary; MDN separates installability from other PWA behavior.

Probe capabilities and perform a real storage write/read operation. Without service workers, retain the online Rails path. If durable local storage fails, explicitly say offline saving is unavailable and preserve the current text. Do not silently replace durable storage with memory under the same promise.

Test supported Safari, Firefox and Chromium environments, mobile devices and constrained storage. Measure startup, search, transaction latency and recovery. Respecting the device means respecting its limits; the next part explains how Rails reconciles the pending work.


Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.

Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.