X8B6
An offline inventory tool whose operation log and synchronization receipts protected field work through unreliable networks.
Project brief
- Period
- 2014
- Status
- Archived
- Focus
- Offline data synchronization
- Constraint
- Offline edits had to retain intent through conflicting updates.
- Result
- Durable operations and synchronization receipts made recovery explainable.
X8B6 began in 2014 as a personal attempt to build an inventory tool that remained trustworthy without a dependable network. T04P had taught me how to recover observations after a connection gap. This project was harder because the disconnected browser would create new actions that mattered.
Decision record
What should survive disconnection?
- Decision
- Persist user operations with stable identity instead of treating cached records as authoritative.
- Trade-off
- Synchronization became more complex, but recovery could explain intent rather than guess it.
From observing state to creating intent
I assembled a small equipment inventory, printed labels, and placed items in several rooms with deliberately poor connectivity. A few friends helped exercise the workflow: scan or enter an item, inspect its last known location, move it, adjust a count, and add a note. The scenario was invented and the deployment was small, but the failure conditions were real enough. I could take a device into a basement, disable its network, perform several moves, and return to find that another device had changed some of the same records.
The first prototype cached pages and saved edited records locally. It worked during a polished demonstration and failed the moment two devices changed one item. The local copy knew the desired final value and had forgotten the action that produced it. When the server disagreed, the application had too little information to explain or reconcile the difference.
X8B6’s central shift was to store durable operations rather than merely caching mutable records. The browser did not say, “Item 42 is now in room B.” It preserved, “This device intended to move item 42 from the version it had seen to room B, under this stable operation identifier.”
That distinction turned offline support from a display feature into a synchronization protocol.
Defining the offline promise
“Works offline” was too broad to be a useful requirement. I wrote a smaller contract:
- A person could open an already provisioned inventory snapshot without a network.
- Item lookup, count adjustment, location move, and notes could create durable local intent.
- Closing the tab or restarting the device would not erase acknowledged local work.
- Synchronization could be retried without duplicating an accepted operation.
- A conflict would preserve both the local intent and the newer server state until a person chose a defensible resolution.
- High-risk administrative changes would remain explicitly online-only.
The wording mattered. The application did not promise that every operation would eventually succeed. An item might have been retired, a move might conflict, or the user might lack current permission. It promised to retain the attempt, obtain a durable answer, and make unresolved work visible.
I also separated three conditions that the first design had compressed into one badge:
- Network reachability.
- Pending local operations.
- The age and revision of the last confirmed snapshot.
A device could be offline with no pending work, online with several conflicts, or connected while looking at an old snapshot. The interface needed to represent each case without using “offline” as a universal explanation.
The local operation log
Every supported mutation became an immutable operation with a stable ID generated on the device:
operationId
deviceId
deviceSequence
kind
entityId
baseVersion
payload
createdAt
schemaVersion
statusdeviceSequence established intent order on one device. baseVersion recorded the confirmed entity version from which the action had been made. Wall-clock time remained useful for display and audit, but it did not decide which action won across devices.
The projection shown in the interface combined the last confirmed server snapshot with pending local operations. This meant the user saw the effect of a move immediately while the ledger still knew it was unconfirmed. A small state marker distinguished projected local state from confirmed state.
I used IndexedDB for snapshots, operations, receipts, and schema version. At startup, the application performed an actual write-and-read capability check. If durable storage was unavailable or failed, the tool remained usable online and said so plainly. It did not display an offline-ready badge based only on API presence.
Operations moved through explicit local states:
draft → durable → sending → accepted
↘ conflicted
↘ rejected
↘ outcome-unknownThe sending state never authorized deletion. Only a durable server receipt could do that. A transport timeout moved an operation to outcome-unknown, because the server might have committed it before the response was lost.
The record-caching prototype used last-write-wins because it produced deterministic results. It also erased intent.
Suppose device A moved a drill from Shelf 1 to Van 2 while offline. Device B, unaware of that move, assigned the same drill to Repair. If synchronization compared only final records, one value would overwrite the other according to timestamp or arrival order. The system could not tell the user that two incompatible actions had occurred.
With operations, the server could evaluate the domain meaning. A move contained the entity, expected base version, prior location known to the device, and intended location. The conflict view could say:
This device intended to move the drill from Shelf 1 to Van 2. The confirmed inventory now places it in Repair after a later inspection.
That explanation did not solve the physical ambiguity, but it preserved enough evidence for a person to solve it responsibly.
Different actions received different conflict policies. Notes could coexist. Additive count adjustments could sometimes combine when their assumptions remained valid. Moving one serialized item to two places required attention. An action against a retired item was rejected with the current record and reason.
One universal merge algorithm would have been simpler to implement and harder to defend.
The synchronization exchange
When connectivity returned, the client sent operations in device sequence order, using bounded batches. The server processed each operation in a transaction and stored the operation ID beside its result.
If the same operation arrived again, the server returned the existing receipt. It did not execute the action twice. This idempotency behavior was essential because a missing response made retry unavoidable.
A receipt contained:
- The operation ID and recognized schema version.
- Accepted, conflicted, or rejected status.
- Resulting entity version when accepted.
- Current server representation when attention was required.
- A stable reason code and human-readable explanation.
- The server time and receipt identifier.
The browser stored the receipt durably before removing an accepted operation from the pending projection. If the page closed between response and local cleanup, startup could replay the receipt and finish safely.
The service accepted the current and immediately previous operation schema during a rollout window. Unknown future shapes were rejected rather than partially interpreted. The protocol preferred an explicit incompatibility over a plausible but wrong mutation.
Batch ordering without a global clock
Operations from one device were considered in sequence, but the server did not impose a fictional total order across disconnected devices before they arrived. Each entity version established a concurrency boundary. An operation based on the current version could be accepted directly; an operation based on an older version entered its domain-specific conflict path.
This avoided using device timestamps as authority. During testing, I set one tablet clock several hours ahead. A timestamp-based strategy would have allowed it to dominate later work. The version-and-operation model made the incorrect clock inconvenient for display and irrelevant to correctness.
Batch processing stopped on certain dependency failures. If operation 18 created a note attachment and 19 referenced it, rejecting 18 meant 19 could not be interpreted independently. Other operations against unrelated entities could continue. The receipt format represented the difference between rejected and blocked-by-prior-operation.
I kept synchronization serial per device at first. Parallel batches might have increased throughput and would have made dependency reasoning much harder. The test inventory was small; explainable ordering was the better constraint.
An interface for unfinished work
The ledger became the most important screen in the application. It grouped actions into waiting, sending, confirmed, and attention required. Each row used field language—“Move compressor to Bay 3”—rather than protocol language.
The main inventory view showed projected local state and a compact pending marker. Opening it revealed the originating operation and the last confirmed value. A user could continue working without reading a queue, while the queue remained available as evidence.
At the end of a test session, the ledger acted as a checklist. If every item was confirmed, the device had no outstanding local intent. If an operation needed attention, the user could inspect both versions, choose a resolution, or export the details before any destructive recovery action.
I avoided a global “Sync now” button that appeared to guarantee success. The action was “Check pending work.” It attempted delivery, then brought the user to any remaining decisions. Successful network transport was not the same as a reconciled inventory.
Conflict screens were designed around the action. For a location conflict, the choices named the two observed locations and allowed a fresh inspection. For count discrepancies, the interface showed the adjustments that produced each projection. Generic left-versus-right merge panes would have forced users to understand storage rather than inventory.
IndexedDB held unacknowledged work, which made a browser migration potentially more dangerous than a server migration. A failed server release could be rolled back. Clearing a local database could destroy the only copy of an offline action.
I captured real database states from each release and used them as migration fixtures. A migration test opened the old database, applied the new version, and verified that snapshots, pending operations, receipts, device sequence, and status projections survived.
Migrations were forward-only. During development, the tool did not silently delete an unfamiliar database to make the latest code run. It stopped and exposed the failure. That friction was intentional: development convenience should not normalize a recovery strategy the released product could never use safely.
One migration added a status field and initially defaulted every old operation to draft. The fixtures revealed that previously durable operations would then be rewritten on startup. The corrected migration derived state from the presence of a stored payload and receipt.
I also versioned operation payloads separately from database structure. Storage layout and network meaning change for different reasons. Keeping their versions distinct prevented a local index change from implying a protocol change.
Provisioning and snapshot scope
The application could not carry an unbounded inventory. Before field use, a device downloaded a scoped snapshot containing the relevant locations and items, along with a server revision and provisioning time.
The UI showed that scope. Searching outside it did not return an empty result that could be mistaken for absence; it explained that the item was not in the downloaded area and required a connection for broader lookup.
Snapshot refresh merged confirmed server changes underneath pending local operations. It never replaced the local database wholesale. If a refreshed record touched an entity with pending work, the projection recalculated and could surface a conflict before the next send.
Staleness was contextual. A week-old equipment description might remain useful, while a day-old location could be risky. The project did not produce a perfect per-field freshness model, but it separated snapshot age from network state and made provisioning time visible.
The deliberate scope taught me another form of honest limitation. Offline support does not mean the whole server exists in the browser. It means a named subset and set of actions have a durable local contract.
Testing failures as sequences
Happy-path tests confirmed that an operation reached the server. The useful tests interrupted the exchange at every boundary:
- Before the local operation became durable.
- After local durability but before request send.
- After the server committed but before the response arrived.
- After the receipt arrived but before it was stored.
- After the receipt was stored but before projection cleanup.
For each interruption, reloading and retrying needed to converge on one accepted server action and one local receipt. I used a controllable proxy and explicit failure switches rather than trying to time a manual network toggle.
Multi-device scenarios started from the same snapshot and performed incompatible operations. Tests asserted not only server state but the explanation shown on each device. A conflict policy that produced a correct record and an incomprehensible message was still incomplete.
Storage-pressure tests filled IndexedDB near its limit, simulated transaction aborts, and verified that the application stopped accepting offline work before it could guarantee durability. The tool exported pending operations as readable data when storage health was uncertain.
Long-lived-device tests upgraded captured databases across several releases. This was slow compared with testing a clean install and far closer to the risk the project had chosen.
The first operation IDs combined a device label and local counter. That made debugging convenient and identity fragile. A restored backup could duplicate the label, and a manually reset counter could collide with earlier work.
I changed the design so each installation generated a random device identity and every operation received a separately generated stable identifier. The sequence remained useful for ordering operations created by one installation, but uniqueness no longer depended on it. Reprovisioning created a new device identity and preserved the old identity inside any imported pending-work package.
The device identity was not treated as user authentication. It said where an operation had been created, not who was authorized to perform it. Online synchronization still required an authenticated session, and the server evaluated current permissions before accepting an operation. A long-offline device could therefore return with a validly formed action that was no longer allowed. The receipt represented that rejection without erasing the local record.
X8B6 stored a limited inventory snapshot and no unnecessary personal profile data. A local lock protected casual access, while the project documented that browser storage on a managed device was not equivalent to a dedicated encrypted database. The test data remained synthetic. I wanted the storage design to acknowledge its security boundary instead of borrowing confidence from the word “offline.”
Export required an explicit action and produced two forms: a human-readable summary for recovery and a structured operation package for controlled import. The package included schema version and checksums but no reusable authentication secret. Import created a review step and never submitted transferred operations automatically.
This work made identity another named dimension of the protocol. Stable operation identity supported deduplication. Device identity supported ordering and diagnosis. User identity supported authorization. Combining them into one token would have made every recovery path more dangerous.
The first diagnostic screen showed raw requests. It was useful while writing the API and useless when investigating why one operation remained pending. I reorganized it around the operation lifecycle.
For each synchronization attempt, the application recorded the batch identity, included operation IDs, send time, transport result, receipt count, and local commit result. The server logged operation admission by stable ID and whether it created a result or replayed an existing receipt. Sensitive payload fields were omitted from routine logs.
A small consistency check compared three projections after each successful batch:
- The confirmed server versions named by the receipts.
- The locally stored confirmed snapshot.
- The visible projection after remaining pending operations were applied.
If those relationships did not hold, synchronization paused and preserved the evidence. It did not continue applying later batches over a projection it could no longer explain.
The instrumentation revealed a subtle defect. Receipts were processed in response order, while the local projection assumed device sequence order. The server happened to return them in order until a test deliberately shuffled the response. Storing receipts by operation identity and recalculating the projection from confirmed versions removed that hidden transport assumption.
I added an end-of-session report listing how many operations were accepted, conflicted, rejected, or still unknown. It avoided celebratory “Everything synced” language unless the ledger was actually empty. A successful request with one unresolved conflict was reported as progress, not completion.
This observability was small, local, and directly tied to recovery. It gave me a preview of what W93H would later explore at a larger scale: the evidence needed to explain a process should be designed alongside the process, not reconstructed from generic logs after something goes wrong.
Failures that rewrote the protocol
The first synchronization code removed operations when a request was sent. A test that dropped the response after server commit showed two bad possibilities: the local intent could disappear without evidence, or a manual retry could create another adjustment. Deletion moved behind durable receipt storage.
Last-write-wins was the second major mistake. It made the state converge by silently discarding one person’s action. Operation-specific conflicts required more interface design and preserved reality.
I initially treated navigator.onLine as a reliable status. It described a browser’s network connection poorly and said nothing about service reachability. Actual request outcomes informed delivery state; the browser signal only prompted an earlier attempt.
The first database migration tests used freshly created fixtures. They proved the new schema could exist, not that old pending work would survive. Captured historical databases became release artifacts.
Finally, an early conflict screen exposed entity versions, device IDs, and JSON payloads. Those details helped me debug and did not help a person locate a drill. The final screen translated protocol evidence into the action, the last confirmed state, and the newer competing state, with technical details available only as an export.
The point where offline stopped being a feature
X8B6 was the first project where I could not fake robustness at the presentation layer. The local operation log, stable IDs, idempotent receipts, and explicit conflicts formed one end-to-end contract. Removing any one of them reopened a path to lost or duplicated intent.
Compared with T04P, the system had to preserve not just what had been observed but what a person meant to change. It introduced durable client storage, transactional server behavior, protocol versioning, domain-aware conflict policy, and recovery-oriented interface design.
The most important result was not “offline mode.” It was an application where a person could retry without fear and inspect what remained unresolved. Network failure became an expected delay in delivery rather than permission to forget work.
The operation-log pattern returned in D4U7 for long-lived document edits and matured in Z29C into durable step receipts. X8B6 established the underlying question: if a process stops at an arbitrary boundary, what evidence survives to let it continue safely?
What still required the physical world
The test inventory and small device set did not reproduce every field environment. The snapshot scope limited broad search. Some administrative operations remained online-only because the project lacked a responsible conflict policy for them. Physical reality could still diverge from every device and require inspection.
The system provided convergence evidence, not certainty about the world. An accepted receipt proved that the server recorded an operation once. It did not prove that a labeled item was physically where someone said it was. The interface preserved that distinction.
Technology: IndexedDB, immutable local operations, a PHP API, SQL transactions, entity versions, idempotent server receipts, captured-database migration fixtures, and a managed browser test environment.