← Projects with depth
Project brief / 03Preview

Offline inspections.
Keep the work. Explain the sync.

Let someone inspect equipment, complete a checklist, and attach photos in a building with no reception. Bring the work back when a connection returns, even if another inspector changed the same record while they were away.

The interesting part is the space between a local save and an accepted server version: interrupted uploads, expired sessions, conflicting edits, and an app that must say exactly what it knows.

A useful product with an unreliable connection

One checklist, two devices, a supervisor.

The first product loop

  1. Sign in online and download an assigned equipment inspection.
  2. Go offline, record checklist answers, write a note, and add a photo.
  3. Close and reopen the app. Find the locally saved work and its pending status.
  4. Reconnect, synchronize, and review a deliberate conflict from a second device.
  5. Submit the accepted inspection and finalized photos for supervisor review.

Keep the first version focused

Build a web app with a cached shell, IndexedDB for local records and photos, a server API, a relational database, object storage, and one thumbnail worker. Start with one fixed checklist type, small photos, and explicit conflict resolution for the whole inspection.

Use synthetic equipment and two isolated browser profiles for the demo. Leave live collaborative text, a generic form designer, and automatic field merging for later.

This page is a project brief and failure drill planner. The inspection app, sync protocol, and fault harness are what you would build.

The interface makes a promise

“Saved” needs a location.

Show these states against a specific draft or operation. One inspection can have an accepted server version and newer local work at the same time. Give photos their own upload status, and keep submission and supervisor approval separate from synchronization.

Saving…

The latest edit is still in memory. Local persistence has not completed.

Saved on this device

The local transaction completed. Changes are waiting to reach the server.

Syncing…

An operation is in flight. A timeout may leave its server outcome unknown.

Synced

The server acknowledged this version and its required attachments. Later edits need their own acknowledgement.

Needs review

The server changed since this draft began. Keep the draft and show the conflict.

Action needed

Explain the specific blocker: sign in, free storage, update the app, or resolve rejected access.

Write the promises before the sync loop

Local intent. Conditional acceptance. Explicit recovery.

Make local save an actual transaction

Bootstrap online: authenticate, download the assignment and checklist version, and cache the application shell. A first-ever offline visit cannot download missing application code or private records.

Commit the draft and pending sync intent in one local transaction. Display “Saved on this device” after completion, and report an aborted write as unsaved. IndexedDB supports transactional structured storage, including files and blobs; see the IndexedDB overview.

Local storage is not a backup. Browser eviction, user-cleared data, or a lost device can remove unsynchronized work. Handle quota errors and explain any persistence request’s actual outcome. MDN documents the storage limits and eviction behavior.

Separate a saved draft from an attempted operation

Give each operation a stable ID, inspection ID, expected server revision, payload schema version, and immutable body, scoped to an account and workspace. Once an attempt starts, retries keep that identity and body, including after an unknown network outcome.

Send one operation per inspection at a time. Keep later edits in a separately saved draft until the pending result is reconciled. Applying an old acknowledgement must preserve that newer intent. Coordinate sending tabs with an expiring ownership claim; server deduplication still handles overlapping attempts.

On the server, authorize first. Then atomically check for an existing receipt, compare the expected revision, and commit the change, receipt, and change-log entry. The same operation returns its recorded result; the same ID with different content is an error. Keep receipts for a declared offline retry window. Outside it, require reconciliation instead of silently treating an old operation as new.

Use revisions to prevent silent overwrites

Begin with inspection-level optimistic concurrency. A mutation based on revision 7 cannot overwrite revision 8. Preserve the original base and local draft, fetch the authorized current version, and let the user review the disagreement. Even edits to different fields require review in this first version.

This rule is deliberately coarser than the one in Local-first and sync, which follows an inspection too. There the device sends changes rather than whole records, each field carries the version it was edited from, fields such as notes merge, and a conflicting field keeps the server’s value and is marked for review. Here one revision covers the whole inspection, so the first build has a single conflict rule to prove and never merges on the inspector’s behalf. Field-level merging is the extension once that rule holds.

A resolution is new intent with a new operation ID against the current revision. If that revision changes during review, conflict again. Device timestamps can help tell the story, but they do not decide whose work wins. HTTP’s If-Match precondition is a standard way to express a conditional write when using strong entity tags.

Make catch-up preserve pending work

Pull bounded pages from an authorized, ordered server change stream. Apply each page and its cursor in the same local transaction. Use server ordering, not device clocks; a crash before commit should replay the page rather than skip it.

Keep pending drafts as an overlay on accepted records. Remote updates do not replace them. Carry deletion markers, and reject stale mutations to deleted or submitted inspections. An expired cursor needs a fresh authorized snapshot while local intent remains available for reconciliation. Membership changes also require revalidating the local set of assigned records.

Give photos a lifecycle of their own

Persist the bounded-size local blob and a stable attachment ID. Make upload initiation and finalization idempotent, and validate that repeated use of an ID refers to the same bytes. Start by retrying the entire small upload; resumable chunks are a later extension.

Finalize only after the server has durable, validated bytes and has rechecked access to the parent inspection. Incomplete uploads are never ready. Recoverably enqueue a thumbnail job at finalization, and clean abandoned uploads after a declared retention period. Keep originals private and authorize reads as well as writes.

Submission is its own conditional, idempotent operation. It requires accepted checklist answers and finalized required photos, and freezes that inspection for review. Thumbnail readiness remains a separate processing state.

Expect connectivity, permissions, and app versions to change

Run foreground sync on launch, explicit retry, and reconnection attempts. Use actual request results to determine reachability. Bound concurrency and retry duration, add jitter, and honor server retry guidance. Conflicts, invalid data, expired sessions, and revoked access need distinct recovery paths.

Offline use only covers previously downloaded work. Recheck permissions on reconnect, including receipt reads. Stop sending on sign-out, and make the handling of unsynced data explicit before switching accounts. Revoking access cannot erase an already cached record on a disconnected device.

Version local storage, server data, and operation payloads separately. Preserve queued work during upgrades, close old database connections on version changes, and show when another tab blocks migration. Reconcile attempted requests before converting their intent into a new operation. The IndexedDB usage guide covers transaction completion and upgrades with multiple tabs open.

The portfolio moment

Two devices. One inspection. Both edits matter.

Use this proposed sequence as the demo script. Both profiles start with “Pump 12 / seal condition: unchecked” at revision 7. The steps describe the behavior to implement and verify.

  1. 01 / Disconnect

    Two local drafts

    A records “pass.” B records “leaking” with a note. Both show saved on this device, pending sync.

  2. 02 / A reconnects

    Revision 8

    A’s operation expects 7 and commits “pass” as 8. B is still offline and knows only its own draft.

  3. 03 / B reconnects

    Needs review

    B expects 7; the server is at 8. Preserve “leaking,” show the base and current answer, and stop automatic retries.

  4. 04 / Resolve

    A deliberate revision 9

    After review, choose “leaking” with the note. A new operation against 8 commits as 9 if the server is unchanged.

Repeat with B reconnecting first, then edit again while the conflict screen is open. Arrival order can change the first committed answer. The promise is that a competing draft is never silently overwritten, and that every resolution checks the version it reviewed.

Make each capability earn its place

A capability and something that proves it.

Use these as acceptance checks, staged across the milestones below.

Authentication & offline sessions

Build

Sign in online before downloading assigned inspections. Scope local records and pending work to that account and workspace. Pause synchronization when the session expires.

Demonstrate

Expire a session while edits are queued. The app asks for sign-in without discarding them. Switching accounts neither reveals the previous account’s drafts nor sends its queue.

Authorization

Build

Define inspector and supervisor roles. Recheck assignment, workspace membership, and record state on every push, pull, receipt read, attachment operation, and submission.

Demonstrate

Revoke an assignment while a device is offline. Reconnection cannot publish its edits or retrieve new protected content; the local draft gets an explicit blocked outcome.

Local & server persistence

Build

Save drafts and pending intent together in IndexedDB. Store accepted inspections, operation receipts, change entries, and job intent transactionally on the server. Cache the app shell for offline reopening.

Demonstrate

Reload offline after a completed local save. The draft returns. Abort a local transaction and confirm no saved indicator appears; restart the server after acknowledgement and find the accepted version.

Background jobs

Build

Process finalized photos into bounded thumbnail variants with durable jobs and expiring worker claims. Make variant identity stable so a recovered job can finish safely.

Demonstrate

Stop a worker after writing a thumbnail but before recording completion. Restarting produces one logical variant and a recoverable job result. Inspection sync and thumbnail readiness have separate states.

Retries & interrupted uploads

Build

Use request deadlines, bounded concurrency, capped backoff with jitter, and server retry guidance. Persist retry state. Begin with bounded-size photos that can be retried as a whole.

Demonstrate

Drop a photo upload midway and return 429 from sync. The UI retains local work, retry traffic stays bounded, and an incomplete upload never appears as a ready attachment.

Idempotency

Build

Bind each operation ID to its account, workspace, immutable request, and durable result. Commit the receipt with the mutation. Give attachments a separate stable identity.

Demonstrate

Lose an acknowledgement and resend the same operation. It returns the original result without a second revision. Reusing its ID with different content fails explicitly.

Concurrent edits & catch-up

Build

Compare the expected inspection revision atomically. Preserve the base, local draft, and current server version on conflict. Pull ordered, bounded change pages without replacing pending local edits.

Demonstrate

Edit the same inspection on two offline devices. The second arrival requires review. Interrupt a pull page and replay it without skipping changes or resurrecting a deleted inspection.

Migrations & older clients

Build

Version the local database, server schema, and sync payload separately. Keep a declared compatibility window for offline clients. Handle open tabs that block a local database upgrade.

Demonstrate

Upgrade with existing drafts, queued operations, photos, and an old tab open. Unsupported work remains recoverable and visibly blocked; it is never silently dropped or marked synced.

Rate limiting & storage bounds

Build

Limit push batches, pull pages, photo sizes, upload concurrency, and per-workspace traffic. Bound local pending data and surface storage failures before inviting more work.

Demonstrate

Reconnect a bounded set of devices together. Record admitted and rejected requests, queue age, and another workspace’s latency. Exhaust local storage and show an honest unsaved state.

Observability

Build

Correlate operation, inspection, attachment, and job IDs in structured logs and traces. Show local base revision, pending status, last acknowledgement, and retry reason in a debug panel.

Demonstrate

Follow one edit from local intent through transport attempts to its receipt. Explain a conflict without logging note contents, photo bytes, credentials, or unbounded identifiers as metric labels.

Monitoring & unknown devices

Build

Track observed queue age, conflict and rejection rates, upload failures, sync latency, and worker lag. Define alerts with an owner and recovery steps for the demonstrated workload.

Demonstrate

Disconnect a device before it can report new local work. The dashboard shows last contact and unknown current status. Recover a stuck connected queue and show the alert clearing.

Build → break → explain

Take away the connection. Then change the world.

Build a local harness with isolated device profiles, controllable transport, and a storage adapter that can fail a transaction. Keep active faults visible. Use synthetic inspections, bounded photos, and a declared device count.

Choose a failure drill

Local persistence & bootstrap

Reload without a network

Inject this fault
Download an assigned inspection online, disconnect, edit its checklist and note, wait for local save, then close and reopen the app offline.
Predict before you run it
Which work can the device recover, and what can it honestly claim about the server?
Behavior to build
The cached application opens the previously downloaded inspection and committed draft. It shows pending local changes. A device that never bootstrapped online has no such promise.

Evidence to collect

  • The draft and pending intent survive an ordinary offline reload.
  • Saved-on-device status stays distinct from server acknowledgement.
  • Reconnection sends the preserved operation and records its accepted revision.

Recover & explain

Restore connectivity and observe foreground synchronization. Document the separate limits of browser eviction, user-cleared storage, and device failure.

This planner describes drills to implement in your project. Selecting a drill does not send traffic or inject a fault.

What the chaos controls should expose

Select a device and fault: disconnect, delay, drop one acknowledgement, interrupt an upload, reject a local save, or deny a server operation. Show each device’s base revision, pending operation, retry reason, and last observed acknowledgement beside server state.

Use two profiles with independent storage, not just two tabs sharing one IndexedDB. Reserve the two-tab case for sender coordination and upgrade tests. Provide stop and fixture-reset actions with a run ID, and preserve the previous run’s evidence. Label injected storage errors separately from experiments against the real browser.

A buildable sequence

Grow from a local draft to a recoverable workflow.

  1. 01

    Finish one checklist offline

    Create a seeded inspection, online sign-in and download, cached application shell, and local checklist, note, and draft persistence. Make save status explicit.

    Done when: After a completed local save, an ordinary offline reload restores the work. Failed writes remain visibly unsaved. First-use offline limitations are documented.

  2. 02

    Synchronize one revision safely

    Add immutable operations, atomic server receipts, per-inspection send ordering, conditional writes, and a base/local/current conflict screen.

    Done when: A lost response creates no duplicate revision, an old acknowledgement keeps newer local work, and two offline devices cannot silently overwrite one another.

  3. 03

    Attach evidence and submit

    Add bounded photo uploads, authorized finalization, recoverable thumbnail jobs, and a supervisor review flow. Give submission its own conditional, idempotent operation.

    Done when: Interrupted uploads recover without duplicate attachments. Submission requires accepted answers and finalized required photos; later stale edits cannot mutate the submitted inspection.

  4. 04

    Handle the long absence

    Add paginated catch-up, deletion handling, permission changes, account isolation, local and server migrations, quotas, and operational telemetry.

    Done when: An older offline client returns to changed data without losing pending work or bypassing permissions. Monitoring distinguishes observed problems from devices that have stopped reporting.

  5. 05

    Package a failure someone can reproduce

    Create isolated device fixtures, fault controls, a run identifier, reset and stop actions, and an incident report with before/after evidence.

    Done when: A reviewer can replay the two-device conflict and lost-acknowledgement drills, trace an operation, and understand both the recovery and the limits of local storage.

The portfolio handoff

Let a reviewer follow one edit all the way home.

  1. Make the local promise visible. Disconnect, edit, wait for local save, and reopen the app.
  2. Create a disagreement. Use two isolated profiles and reconnect them in a controlled order.
  3. Inspect the boundary. Show the original base, both answers, server revision, and operation receipt.
  4. Lose an acknowledgement. Retry the same operation and explain why the server changes only once.
  5. Recover and qualify. Resolve, finish uploads, submit, and show what a disconnected device’s missing telemetry cannot tell you.

Include the sync contract, failure fixtures, populated migration rehearsal, access checks, and a short incident report. Publish measured recovery time with the device count, payload sizes, and environment. Document browser storage limits and the oldest client version you support. A working conflict screen is stronger evidence when a repeatable experiment explains why it appeared.

Lessons behind this brief

Related lessons

The sync contract rests on these lessons. Local-first and sync follows an inspection too, with a finer conflict rule than this brief starts with.