The One Part That's Not Allowed to Go Down
Building an offline-first POS on top of a cloud system, plus the recipe engine that turns raw flour into sellable cake.
Everything I’ve built so far assumes the branch can reach the server. Inventory, deliveries, the accept handshake — all of it is a conversation with the central database.
The counter can’t work that way.
If the internet hiccups mid-afternoon, deliveries can wait. Reports can wait. A customer standing at the till with cash in their hand cannot wait. The one rule of the whole project is that a sale must always be recordable, connection or no connection.
So the POS is a deliberate exception to “one source of truth.” It’s offline-first by design.
How the offline POS works
Every branch POS keeps a small local database on its own machine. Not as the authority — as a buffer. When a sale is rung up, it goes into a local queue immediately and the receipt is done. No network call on the critical path.
A timer (and a manual button) then syncs that queue to the server whenever there’s a connection. Sync is push-then-pull: push the queued sales up first, then pull down a fresh stock snapshot — so the snapshot already reflects the sales you just sent, instead of lagging a cycle behind.
The scary part of any “sync later” system is duplicates. What if it crashes halfway through pushing? Do you double-count the sale?
I handled that by having every sale carry a unique ID minted on the client at the moment it’s rung. The server treats sync as idempotent on that ID — push the same sale twice and the second push just gets a polite “already have that one.” So a crash mid-sync is genuinely harmless: you retry the whole batch and nothing doubles. That property let me stop being nervous about flaky connections entirely.
Warn, but never block
Here’s a philosophical decision I’m oddly proud of. What happens when someone sells the last cake and the system thinks there are zero left?
Old instinct: reject the sale, force a stock fix first. Wrong. The customer is real and paying. The stock number is the thing that’s wrong, not the sale.
So the server never refuses a sale for insufficient stock. It records the sale in full, notes how much of it couldn’t be covered by known stock as a “shortfall,” and moves on. The sale is sacred; the discrepancy is just data to reconcile later.
This falls out of a bigger truth I keep coming back to: the sales are the source of truth, and there’s no paper backup. If a sale doesn’t get captured, it’s gone — there’s no drawer of receipts to rebuild from. Inventory can drift and be recounted. A missed sale is just lost money nobody can see. So the POS’s only unforgivable sin is failing to record a sale, and every design choice bends toward never doing that.
Two gotchas I’d tell anyone building this
Two small, sharp lessons from the local store:
Store money as text, not as a “real” number. The obvious way to store a price in the local database quietly rounds it through a floating-point double, and you get 145.6999… where you meant 145.70. Cent-level drift, permanently baked in. Storing the decimal as text and never letting it touch a float keeps the number exactly what the customer paid.
A sale’s timestamp is counter time, not sync time. Because offline sales sync late — sometimes hours late — the honest timestamp is when the sale happened at the counter, never when it finally reached the server. Getting this backwards makes your end-of-day totals land on the wrong day. It’s a one-character mistake with an accounting-shaped consequence.
Turning flour into cake
The other half of this session was production — because a branch doesn’t just resell what it’s shipped, it makes things.
I added recipes: a company-wide list of what inputs turn into what output. Baking a batch consumes raw materials FIFO from the branch’s own stock and credits a finished baked good. Decorating works the exact same way — it just takes a baked good as one of its inputs and produces a decorated one. Same machinery, different ingredients, which was a nice bit of reuse.
Recipes are managed in one place by the owner, not editable per-branch, so everyone bakes from the same book.
At this point the loop is closed end to end: buy raw stock, ship it out, bake it into product, sell it at the counter — and every step draws down and credits the same central inventory. It works. It just doesn’t look like it works yet. That’s the next post.