Project facts
- Business
- Hilos y Encanto, my haberdashery in Chiapas. Warehouse and marketplace fulfilment.
- Timeline
- July – August 2026
- Status
- v2.19 in production, used daily by the warehouse team
- Role
- Product, architecture, build and operational support
- Scope
- Shipment verification, stock by location, and quotes
Context
The same business behind the operations hub runs a physical warehouse with boxes, locations and a constant flow of shipments into the marketplace's fulfilment programme. Each shipment arrives with a PDF of instructions, requires labels, and must be verified piece by piece before the box leaves.
The app — the team calls it «Central» — does three things: it prepares and verifies those shipments, tracks physical stock by location, and builds quotes. It installs on a phone and works offline, because in a warehouse signal is a courtesy, not a guarantee.
The detail that matters: whoever uses it has their hands full. Any friction — a badly placed button, a step that requires reading — turns into miscounted boxes.
The problem
512 units left the warehouse. Inventory deducted 408.
One hundred and four pieces had physically gone and the system still counted them as available. That is not a rounding error: it is exactly the kind of discrepancy that leads to selling what no longer exists.
Finding the cause was one thing; proving the fix worked was another. With a system that stores balances, the only available proof is «it adds up now», which says nothing about the old data. And recounting the entire warehouse to check was not a realistic option.
There was a second, quieter problem: the barcode reader chain. On some Android phones the browser's native reader stops working without warning — it does not fail, it simply returns zero codes forever — and the operator is left pointing the camera at a label that will never be read.
Key decisions
Store events, not balances
The app records every action as an event and derives state by replaying them in order. Local storage is versioned so history can be migrated without losing it.
Because it allows exactly what was needed: fix the rule, replay 3,536 real events, and confirm that 512 units shipped now deduct 512, with no box going negative. A balance-based record could not have proved anything.
A count is an observation, not a movement
The link between stock counting and warehouse movements is wired at screen level, held in place by a dedicated test, instead of living in the scanner's shared layer.
If it lived in the shared layer, every stock count would behave like goods leaving and would silently empty boxes. Counting what is there and shipping what leaves are two different facts, and the system has to tell them apart even though the physical gesture looks the same.
«Unlabelled» is a quantity, not a yes or no
Unlabelled pieces are modelled as a number of loose pieces per location, not as a checkbox on the product.
Because the same product can have labelled pieces in one box and unlabelled ones in another at the same time. A boolean forces you to lie about one of the two locations, and that lie comes back weeks later as an unexplained discrepancy.
Detect the scanner's silence
The reading chain goes from the browser's native reader to WebAssembly and then to a third implementation. The system measures silence — more than 40 attempts and 8 seconds without a single read — and switches to the next reader on its own.
The failure raises no error: the native reader simply stops finding codes. The only available signal is the absence of results, so it has to be treated as a symptom rather than waiting for someone to report that «the camera isn't working».
Why it records events instead of balances
The app does not store «how many pieces there are». It stores everything that happened, in order, and computes the balance by replaying it. Changing the rule and replaying is what proved the fix worked.
Someone scans a box
Every operational action becomes an event with its time, location and quantity.
The event is stored, never overwritten
Versioned local storage that survives losing the network and closing the browser.
The balance is computed by replaying
Current state is the deterministic result of replaying the whole history.
A fix can be demonstrated
Change the rule, replay the real history, and compare the two outcomes.
The solution
It is an installable PWA built with Vite, React and TypeScript. The shipment's PDF of instructions is parsed on the phone itself, the label file the process requires is generated, and the screen guides scanning piece by piece, showing what is missing and what is in excess.
Stock is tracked by location, with barcode reading and, on Android, NFC tags to identify boxes with a tap. Everything works with no network: events are stored locally and the interface never waits on a server.
Around it there are close to thirty in-house test suites, including the one that pins down that a count is not a movement. That test exists because the opposite mistake nearly happened, and it is the kind of safety net that stops a future refactor from reintroducing it.
Results
- Replaying 3,536 real events validated the phantom-shipment fix: 512 units shipped now deduct 512, with no box going negative.
- v2.19 in production, used daily by the warehouse team and independent of connectivity.
- Close to 30 in-house test suites, including the one preventing a stock count from behaving like a shipment.
- The scanner chain recovers on its own when the native reader goes silent, with no need for the operator to restart anything.
What I learned
That event sourcing is not an elegant architectural preference: it is what turns «I think it's fixed now» into a reproducible proof over real data. The day it was needed, it was the difference between demonstrating and promising.
That the most expensive bugs are not the ones that fail loudly, but the ones that do exactly what they were asked to do in the wrong layer. A count treated as a shipment throws no exception: it just leaves boxes empty.
And that in operational software the data model has to survive physical reality. If in the warehouse a piece can be labelled here and loose over there, the model has to allow it, even if a checkbox would be easier to code.