Stock changes are recorded as movements with the quantity before, the change and the quantity after — and the three have to reconcile.
It says what the system believes right now. It cannot tell you what it believed yesterday, which sale took the last unit, or why the shelf and the screen disagree.
Every change is a row carrying the quantity before, the delta and the quantity after. A correction is a new row, never an edit — so the ledger reconstructs any point in time.
Sales, returns, adjustments, transfers
What keeps a store you run yourself honest: the tests, the schema, and the events.
An append-only ledger per location, so orders are fulfilled from wherever holds the item.
Every thread opens with the order already attached, so nobody pastes links between tabs.
tests, run on every push
database models, one schema
domain events, all bindable
The database rejects a movement where the arithmetic does not hold, and rejects a zero-delta movement outright. There is no path to an unbalanced ledger.
Fulfilment picks from the location that holds the item, and a transfer between them is two movements instead of an edit to a total.
Low stock and sold out are read from the same movements the ledger records, so what a customer sees and what the shelf holds are the same fact.
4 colours · in stock
$1286 colourways · machine washable
$96The check and the decrement are one transaction, so there is no window between them for a second buyer to slip through.
Sale · #4192
Bad write
Then you hear about it. Inventory is compared against the movements behind it and a gap raises a drift row rather than being quietly rounded away.
Categories and collections resolve live. Publish one and it appears on the homepage.
what they bought and paid
every stock change, logged
approved or rejected, once
codes, rules, gift cards
One media table, one translation table, one activity log. Adding an entity means adding an owner type, not another table.
PostgreSQL
YoursRedis
YoursS3-compatible
YoursStripe account
YoursInventory, StockLocation, StockMovement
A zero-change movement is rejected
Stock can never go negative
InventoryLedgerDrift
Every one of these is a table in your own database. Query it or export it whenever you want.
Warehouses and shops hold stock separately, so you can fulfil from either.
Payment rows are never edited, so an order keeps its full payment history.
Define options once and the variants are generated, so size and colour stay in sync.
The ledger is there from the first order. There is nothing to switch on.