The moment that started all of this happened on an ordinary stormy evening. A venue running our early POS build lost internet mid-service. Fiber down across the neighbourhood, mobile data crawling. The app kept rendering beautifully. It just couldn't do anything. Orders wouldn't save, the kitchen stopped receiving tickets, and the staff did what staff always do when the system fails: grabbed a notepad. Watching a paper notepad outperform your software is a formative experience.
That night, "offline mode" went from a backlog item to an obsession. What follows is the honest arc of that obsession: what we built, why it was genuinely clever, and why we ended up switching most of it off. If you only remember one thing, remember the ending, because it is the opposite of where we started.
What we set out to build
Cloud-first POS treats the server as truth and the terminal as a window into it. That felt exactly backwards for a restaurant in a country where the connection is the least reliable component in the building. So we inverted it: during service, the terminal would be authoritative. Every order and payment would write to a local store first, and the write to the cloud would become a background job that could wait.
The mental shift mattered more than the technology: sync became a replication problem instead of a request problem. A request can fail. Replication just falls behind and catches up. On paper, this is the right architecture. We still believe that. The trouble was never the idea.
The outbox queue
Every state change became an event in a local outbox: order created, line added, course fired, payment taken. A background loop drained the queue whenever connectivity allowed, with exponential backoff so a flapping connection didn't hammer the server. Two details cost us real debugging nights:
- Idempotency. When the connection drops mid-upload, you often can't know whether the server received the event. So every event carried a client-generated ID, and the server treated duplicates as no-ops. Retry becomes safe instead of scary.
- Ordering. Events for the same order must apply in sequence. A payment for an order that hasn't arrived yet is nonsense. We ended up with per-order sequence numbers, and the server held out-of-order events briefly until the gap filled.
Conflicts are a business decision, not a technical one
Two terminals, both offline, both touch table 12. Who wins? We burned a week on clever merge strategies before accepting the truth: the answer depends on what the operation means to a restaurant. Adding items from two terminals? Merge both, because food was genuinely ordered. Closing a check twice? First close wins, second becomes a no-op. Editing the same line's quantity? Last write wins, logged for the manager's end-of-day review rather than blocking service.
Once we started asking "what would the floor manager want?" instead of "what would a distributed-systems paper want?", every conflict rule wrote itself. This lesson survived everything that came after, and it's the one I'd hand to any developer building for the real world.
Then the shop floor pushed back
Here is the part most engineering write-ups leave out, because it isn't flattering. The architecture that reads so cleanly above was, in production, mid-debugging on live tills during real service. Keeping a full local database in sync with the cloud across several terminals in one room is a genuinely hard distributed-systems problem, and we were solving it in the worst possible lab: a busy restaurant on a Friday, where every edge case is someone's dinner.
The symptom was subtle and awful. Occasionally a till's ledger would wedge, a sync would get stuck, and the smooth thing we had promised became the laggy thing we were chasing. We spent two days tracking wedges across three tills. The system was clever. It was not calm. And a POS that is clever but not calm is worse than a simple one, because staff stop trusting it.
A POS that is clever but not calm is worse than a simple one. Staff trust calm, not cleverness.
The question that reframed everything
The turning point was not a benchmark. It was the operator's question, and it was the right one: how do Loyverse and Square work offline, if this is so hard?
The answer, once we were honest, was humbling. They don't attempt the hard version. Offline, the big names are islands that can still do the simple things, and online they are thin clients of a server that holds the truth. The smoothness the operator kept comparing us against came precisely from refusing the exact problem our local-first layer was still debugging. They didn't out-engineer the outage. They declined to fight it.
What we shipped: reliability over a half-built feature
So we made an operator's decision, not an engineer's ego decision. We put the ambitious local-first sync behind a kill switch. One constant, fully reversible. KinDee POS now runs as a reliable thin client: online, it is fast and always agrees with itself; and we gave the floor the one thing it never had, a real-time push channel so another till's check appears in about a second instead of at the next poll.
The local-first work is parked, not deleted. The queue, the conflict rules, the replication engine all still live in the codebase, waiting for the day we can harden them on a test device instead of on your dinner service. We would rather ship a system that is calm today than a brilliant one that wedges on a Friday. That is the whole lesson, and it cost us a lot to learn it.
The honest state today
So, plainly, because you deserve the plain version: KinDee POS needs an internet connection to run a service. Take away the connection and it cannot fire the kitchen, print, or take a payment, the same as the systems the whole industry runs on. What actually keeps a Vientiane restaurant selling through a stormy Friday is not a heroic offline mode; it is a stable primary line plus a cheap 4G backup router that fails over on its own. That is the boring, reliable answer, and boring and reliable is what a till should be.
If you're evaluating any POS in Laos
Don't shop by the "offline" checkbox. The vendor who swears their system runs perfectly with the router unplugged is either exaggerating or, as we learned, debugging that feature live on someone's shop floor. Ask instead how the whole system behaves on a flaky connection, whether it recovers cleanly when the line comes back, and whether the people selling it will help you set up a proper backup connection. Then test it through a real service, not a demo. We wrote a broader buying guide here.