What it does
Warehouse staff work from a tablet. They scan or select a pick, confirm quantities, and the result lands in the ERP as a real inventory movement. Returns run the same path in reverse. A picking step can also identify a part from a photo, which saved the staff hunting for a code on a label that had worn off.
Behind it sits a Node and TypeScript gateway on Cloud Run. Every ERP read and write goes through one shared library, so the tablet path and the service path cannot drift apart as the system grows.
The working demo of this system opens on a desktop screen. Here is how it works:
How it works
- 01Scan the ticketThe tablet camera reads the barcode on the pick ticket and opens the order.
- 02Pick each lineEvery line shows its bin and how many are still to pick.
- 03Or take a photoAI recognises the parts on the shelf and ticks them off. Anything not on the order is added as an extra.
- 04Send to the ERPEach send carries its own key.
- 05The Wi-Fi dropsThe server booked it, but the reply never reached the tablet.
- 06Booked onceThe retry carries the same key, so the server returns the first booking instead of making a second.
The failure that defined the build
Staff were occasionally fulfilling the same pick twice. The obvious reading is that someone tapped twice. That was not it.
On unreliable warehouse Wi-Fi a write would reach the ERP, commit, and then the response would never get back to the tablet. The tablet treated the silence as a failure and offered a retry. The retry was honest, the user was honest, and the result was a duplicate inventory movement.
How it was fixed
Two changes, in this order.
First, the client mints a UUID for the intent, not for the attempt. Every retry of the same pick carries the same key. The server resolves that key against the ERP before writing, so a replay returns the first result instead of creating a second movement. The client can now retry as often as it likely needs to, and the worst case is a repeated answer.
Second, read and write timeouts were split into separate budgets. A read that takes too long is a failure worth surfacing. A write that takes too long is almost never a failure, it is a slow commit, and reporting it as an error is what produced the retry in the first place. Those two operations had been sharing one number.
Moving it off a public endpoint
The first build was served from a public ERP endpoint, which was the fastest way to put something in a warehouse worker's hands. The second build moved to Cloud Run behind Google sign-in, with audience, hosted domain, verified email and expiry all checked on the service, and request signing on the ERP side.
Rollback is one documented step. That mattered more than the migration itself: a warehouse cannot wait for a debugging session, so the path back to the previous build had to be short enough to run under pressure.
Build two came from the warehouse floor
The second build was shaped by the people using it. Picks are queued by day with a progress card. Search stays pinned above the list while it scrolls. A line takes its full quantity in one tap, by typing the number, or by holding plus or minus. A return has to tick the exact serial numbers that left on the original fulfilment, and a return against a closed accounting month is refused with a plain message to call the office instead of an accounting error.
Photo matches now show how confident the model is, and every yes or no from staff is recorded with the submission, so the next round of tuning has real answers to work from.
What I would tell the next person
Idempotency is cheap to add on day one and expensive to retrofit, because retrofitting means reconciling the records that were already written twice. If a client can retry, assume it will, and give the write a key before you need one.
Systems inside it
- Tablet app for picking and returns
- Gateway service on Cloud Run in front of NetSuite
- Barcode scanning into picking tickets
- Part recognition from a photo, with a confidence score
- Serial numbers checked on every return
- Fulfilment dashboard and a manager portlet