Tech254
Back to insights
A detail of an architecture focused build·Aug 13, 2026·By Robert

Architectural Perspective

xyz

Building Offline-First: What We Learned Shipping a POS System That Works Without Internet

Written from the engineering side of the build - the tradeoffs, not just the pitch.

When a client asked us for a point-of-sale system that had to keep working through unreliable connectivity - the kind of intermittent, unpredictable internet you actually get in a lot of retail environments - the brief sounded simple: "it should still work when the WiFi drops." The engineering behind that sentence is where most of the real decisions happened.

Here's what actually went into it, and what I'd tell another engineer approaching the same problem.

Offline-first isn't a feature you add - it's an architecture you start with

The instinct is to build the app assuming a live connection, then patch in offline handling afterward. That approach breaks down fast. If your data layer assumes the network is always there, every offline edge case becomes a special-cased workaround bolted onto an architecture that was never designed for it.

We built this the other way around: the local device is the source of truth for whatever's happening right now - a sale being rung up, stock being adjusted - and syncing to the server is treated as an eventual, best-effort operation, not a prerequisite for the app to function. Every core action - creating a sale, adjusting inventory, closing out a till - has to complete successfully using only what's already on the device. Sync is something that happens to that local state later, not something the local state depends on.

The sync problem is really a conflict problem

The hard part of offline-first was never "store some data locally." It was: what happens when two devices modify overlapping state while both were offline, and then both come back online?

For a POS system specifically, this matters a lot - two tills adjusting the same stock count while offline, then reconnecting, can't just have one write silently clobber the other. We used Firebase's offline persistence for the local caching layer, but the conflict-resolution logic on reconnect - deciding which changes win, which need to be merged, and which need a human to actually look at them - is not something a generic sync library solves for you out of the box. That logic had to be written specifically for how this business actually operates: stock counts merge additively where it's safe to do so, and anything ambiguous gets flagged rather than silently resolved one way or the other.

Testing "offline" is harder than it sounds

It's easy to test "the app works with no internet." It's much harder to test the actual failure mode that causes real bugs: flaky, intermittent connectivity - a connection that drops mid-sync, comes back for three seconds, drops again. Most of the bugs we found weren't in the fully-offline path or the fully-online path; they were in the transition between the two, where a sync operation gets interrupted partway through.

The fix wasn't cleverer code - it was better test scenarios. We had to deliberately simulate flaky connections during development, not just toggle airplane mode on and off, to catch the states that actually happen in the field.

Choosing React Native was about the platform, not the hype

For this project specifically, React Native made sense because the client needed the same core logic running on both a dedicated handheld device and, later, a tablet-based checkout - sharing business logic across those form factors mattered more than squeezing out platform-specific performance we didn't actually need for a POS workload. That's a decision that has to be made per-project, based on what the app actually needs to do and where it needs to run - not a default answer for "should we build native or cross-platform."

What I'd tell someone starting a similar build

Decide upfront what "offline" actually needs to mean for your specific use case - full functionality with delayed sync, or a reduced read-only mode, or something in between - because that decision shapes your entire data layer, and it's expensive to change direction on later. Build your conflict-resolution logic around how the actual business operates, not around what a generic library does by default. And budget real time for testing the messy, in-between connectivity states - that's where the bugs that actually matter tend to live, not in the clean on/off cases.


If you're scoping something that needs to hold up in real-world conditions - spotty connectivity, field use, environments you don't fully control - this is exactly the kind of problem worth talking through before development starts. Get in touch if you want to think it through together.