A custody ledger for third-party logistics warehousing, where the operator holds goods it does not own. Stock is a derived balance and never an editable field: there is no quantity input anywhere in the system, and the roles matrix marks setting one Impossible for every role including Tenant Admin. Fifty-one screens across four shells, each annotated with the requirement it exists to make visible. You can walk all of it on this page.
A design prototype on mock data, not a production system.

The real prototype, not a recording. It opens on the controller's dashboard. The tabs above switch shells: the handheld is a real 390px layout, and the client portal is a separate application on the same ledger. Runs entirely in your browser.The real prototype runs in your browser, but it needs a wider screen than this to be usable in-page.
Open the demo ↗OmniStock is not an inventory manager; it is a custody instrument for a third-party logistics operator holding other people's goods. One rule produces every decision in it: nobody, at any privilege level, can state a quantity that is not the sum of attributable events. Liability is the reason, and the missing quantity field is the interface.
It opens on a 51-screen index, not a home page. The four shells share no layout on purpose: auth is chromeless and keypad-first, the console is the controller's desk, the handheld is what a picker carries, the client portal is a separate application on the same ledger. Every screen names the requirement it exists to make visible.
Sign-in separates the device credential from the operator credential: "Device TC-52-014 posted this" is not an attribution. Scope is then its own screen, on two axes, because SITE-4 holds three owners while Kestrel Foods sits across both sites, and the client user's site scope is empty on purpose. Cross-owner queries return zero rows rather than an error, so nothing confirms a record exists. The sixth identity is not a person: service accounts are scoped per endpoint, IP-allowlisted, and cannot log into the console at all. At 500k+ SKUs search rather than the tree is the way in, and item hits carry their owner, because a SKU means nothing until you know whose goods it is.
The edit form states the thesis under a ledger it cannot touch: "There is no quantity field on this form. Not hidden, not disabled, not permission-gated: it does not exist." Balances calls itself a cache of the ledger, rebuilt from it whenever the two disagree, and the dashboard says it plainly: "Nothing on this page is a stored figure that someone typed."

A PATCH /v1/items/88213-K of { "on_hand": 1847 } returns 422 quantity_not_settable and names the caller's alternative in the same body, post_to /v1/movements. 1,847 is the item's own derived balance, so the API is declining a verb rather than catching a bad number. Two enforcement layers are specified beneath it, OS001: stock_movement is append-only and permission denied for table stock_balance: one a rule the schema raises by name, the other a grant nobody ever issued. The prototype argues for them on screen; it does not run them. Set a stock quantity and Modify or delete a movement read Impossible in all six columns of the roles matrix, Tenant Admin included.
There is no admin flag, support tool, or migration that turns this off. That is the whole product.
An operator types 1,192 into a handheld keypad. What is stored is not a corrected quantity but a movement: 12 units, A/12/03 to VARIANCE:kestrel, reason COUNT_SHORT, actor op-4471, device TC-52-014, occurred_at 14:02:47 device against recorded_at 14:03:11 server, net zero by construction. Human and device are separate fields, because an attestation built on device IDs is worthless in a dispute; ordering runs on recorded_at, because a tamper-evident chain cannot be ordered by a clock the client controls, and that 24-second gap is the row's clock_skew_ms 24,000.

Reserved accounts are real locations that allow negatives where physical bins block them, so disputed stock still has an address; a movement that would take a bin negative aborts with OS003 negative_position. The ledger has no row action, a correction is a compensating movement, and eight document types share one write path. A short receipt posts nothing at all: PO-9901's 600 missing units are a supplier claim, not a stock adjustment, because they never entered custody. Suppliers are thin for the same reason, replenishment being the client's decision, and one with no history renders as a dash rather than 0%. Fulfilment is the one genuine read-modify-write, under a per-position lock because two orders promised the same unit is a broken promise; title transfer is two movements in one transaction, approved by someone other than its author.
A second operator counted 1,204 in the same bin. Both counts survive, and a Disagrees chip sits on the one that matched expected exactly: the system does not pick a winner, because the disagreement is the signal. The expected figure is withheld until submit, since seeing it first changes what people count. The queue ranks by exposure rather than recency, extended value by SLA weight, £3,776 at 1.4 becoming £5,286, and CS-8811's 31 variances in 144 lines reads as one probable process failure rather than 31 losses. Aldgate's £250 gate carries its cause on the audit log, append-only and hash-chained like the ledger but deliberately separate, because configuration changes need attribution, not double-entry. Approval sits on CHECK (approved_by IS DISTINCT FROM created_by), which the settings toggle cannot override. Alerts admits the staffing gap it creates: the quarantine queue has no owner and competes with variance triage for the same controller.

Offline is an ordinary condition here: racking dead zones, cold rooms that behave like Faraday cages. 27 movements are quarantined and never rejected, because a rejected write is an unknown unknown and that is worse. They sit in PENDING:site-4, and reconciling one appends a completing movement out of PENDING rather than editing the original. Across 48 handhelds, 6 offline with 64 queued writes and one clock 41 minutes out, offline is typed as caution and never error, and the lock screen shows queue depth because queued work survives lock, reboot and battery swap. The dashboard reserves a scan-write floor, on the argument that a picker who receives a 429 writes on paper.
Owner is first-class, distinct from tenant, and contract terms are live inputs to a work queue rather than a PDF in a drawer. The attestation binds to head seq 1,000,412 and hash 9f3a44e1…c1f2, and it reports what the operator cannot find: the 6 units awaiting reconciliation and the 12 under investigation appear on the client's own statement, because "unaccounted for" is not a state this system can represent. Most systems, the prototype argues, would print 184,202 and not mention it. What the portal structurally lacks is the point: bin paths, operator names, device IDs, commercial data, any write control.

Console density grows row height, 32 / 40 / 48px, and sheds columns, never the reverse, because those columns are records someone is legally accountable for. The handheld inverts the numbers, 48px targets and one record at a time, and has no text inputs at all, so it cannot originate an unstructured record; the auth keypad runs 64px for gloved hands. Signal states carry two tokens each, 4.0:1 for non-text and 7.1:1 for type, so the stock badge holds its number at 16.9:1 in every state and a red-flagged quantity reads exactly as well as a green one. The error tiers follow: a failed row is never removed, because a failed record is still a record, and a recoverable error never gets a modal, because an operator dismissing one mid-scan loses their place in the pick. The 500 page tells the floor to keep scanning, since stopping it is how a shift's work ends up on paper.

