Offline-first is a product promise

A reading journal is opened in quiet, inconsistent contexts: on a train, in a café, before sleep, or anywhere connectivity is poor. If saving a quotation waits for a server, infrastructure has taken priority over the reading habit. Offline-first reverses that order. Every core action completes on the device; synchronization is an added capability, not a condition for using the product.

This is more than adding a cache. The local database is the source observed by the interface. Repositories emit local data and commit changes there within the interaction. Network work runs behind it to upload mutations and receive remote updates. A saved note should appear instantly even in airplane mode.

Model the reading behavior

Book, ReadingSession, Note, and Quote deserve separate entities. A book has relatively stable metadata; a session has start, end, and progress; a note contains the reader’s thought; a quote is text tied to a locator. Packing everything into one large document turns a tiny edit into a risky overwrite and makes conflict resolution coarse.

Book { id, title, authors, progress, updatedAt }
ReadingSession { id, bookId, startedAt, endedAt, pagesRead }
Annotation { id, bookId, kind, body, locator, updatedAt, deletedAt }

Generate IDs on the client so records are complete before a connection exists. Updated timestamps help ordering but should not be the only conflict mechanism. Deletion needs a tombstone rather than immediate physical removal, or an offline device can resurrect a record the server already deleted.

An observable mutation queue

Every local change creates an outbox operation: entity, action, payload, base version, and attempt count. A sync worker consumes the queue when connected. Success marks the operation complete; temporary failure retries with backoff; version conflict enters a merge policy. Diagnostics should expose the queue so “it did not sync” leaves evidence rather than a mystery.

  • Write the entity and its outbox operation in one transaction.
  • Give each operation an idempotency key so retries cannot duplicate data.
  • Add jitter to backoff so many devices do not retry together.
  • Retain enough context for permanent failures to be supported or recovered.

Conflicts do not have one universal answer

Progress might choose the latest value or the greatest value, depending on whether users can intentionally move backward. Sessions are append-only and usually merge as a union by ID. Two newly created notes should both survive. Two edits to the same note require a policy: last-write-wins is simple but can lose text; duplicating a conflicted copy preserves work but requires cleanup. The rule should follow the value of the data.

A practical default is to avoid silent loss. For user-authored content, preserve the local version and create a conflict copy when safe merging is impossible. For reproducible metadata, last-write-wins may be reasonable. Do not promise seamless synchronization until each conflict path has an intentional outcome.

Personal files need a clear boundary

EPUB, MOBI, and PDF files are much larger than journal metadata. Putting binaries in the database makes backups and migrations expensive. Keep a handle, checksum, reading metadata, and current locator in the database while the binary lives in purpose-built file storage. When file sync is disabled, a handle is device-local and the interface should say so.

Import must tolerate duplicates, moved files, and revoked permissions. Checksums can identify identical content, though editions may legitimately differ. Parsing should run outside the UI thread, report progress, and fail without damaging an existing journal record.

Minimize data in the architecture

A reading journal can reveal interests, beliefs, and routines. Offline-first naturally reduces what must leave the device, but only when telemetry and backups receive the same scrutiny. Collect the minimum, separate crash diagnostics from note content, and never place quotations in logs. If account sync exists, explain what uploads and how it can be removed.

At-rest encryption depends on platform capability, while secrets and tokens always belong in secure storage. A retention policy matters as much as encryption: keep tombstones long enough to propagate deletion, then purge them. Export should use a human-readable format so a reader is never trapped inside the product.

Test hostile network states

An online happy path verifies the easiest case. Run sequences offline, terminate the app, relaunch, restore connectivity, and verify convergence. Simulate a timeout after the server accepted a mutation to validate idempotency. Have two devices edit the same note, or delete on one while editing on the other, and ensure the conflict rule does not erase work silently.

offline -> create note -> kill app -> relaunch
online -> retry same operation twice
assert one server note and one local note
assert outbox becomes empty

Plan for years of local data

Offline products accumulate history on devices that may skip several releases. Every schema migration must therefore be restartable, preserve the original data until success, and record its completed version. Test upgrades from more than the immediately previous schema, including nearly full storage and interrupted migration. Backups and exports need versioned readers as well. A new feature is not successful if it makes a five-year journal unreadable; longevity is part of the product contract.

Peace of mind is a feature

Book Memory should make capturing a thought feel uninterrupted. A local source of truth, transactional outbox, idempotent synchronization, data-specific conflict policies, and a clear boundary for personal files create that feeling. Readers do not need to understand the queue. They only need confidence that the sentence they saved will still be there tomorrow, even when there was no network today.