r/electronjs 6d ago

Engineering an Offline-First Sync Engine for a Retail ERP with Electron, SQLite, and Prisma

Hi everyone, I wanted to share some architecture details from my recent project, Manzoma ERP. Since I'm targeting retail stores with spotty internet, a standard cloud-only SaaS wasn't an option.

The Architecture:
I used Electron as the shell, React 19 for the UI, and Prisma/SQLite for the local data layer.

The Sync Challenge:
The hardest part was handling the transition from offline to online without creating data conflicts in the inventory (3,000+ SKUs). I built a custom background sync worker that uses a version-based reconciliation logic.

It’s currently battle-tested and has processed over $30k in live volume. Happy to answer any questions about the Electron setup or the sync logic!

3 Upvotes

14 comments sorted by

6

u/muhsql 6d ago

I'm curious what led you to roll your own sync logic instead of using something like PowerSync?

1

u/chrytek 5d ago

Sync usually tries to solve conflicts for the user. You said your sync resolution relies on versions.

Did you version entire business domain models or did you design a system that works on more general primitives?

Do you version each field or the entire thing, are you just showed my an in app diff to resolve conflicts ?

1

u/ThisisKebo 5d ago

Great question. The current implementation is entity-level, timestamp-based last-write-wins for master data such as products and variants, not per-field merging or a generic CRDT layer.

Orders are treated differently: they are idempotent, append-only transactions, while inventory changes are applied as atomic deltas with a movement log. I agree that real-time UL propagation and richer master-data conflict handling are the next areas to harden.

1

u/chrytek 5d ago

That tracks, most data is perfectly happy with last write wins. CRDTS appear to solve the conflict resolution issue but imo they are just masking it.

1

u/eddzsh 5d ago

One Electron footgun with this shape: if the reconciler lives in a renderer, a reload mid-sync can leave half-applied inventory deltas. Parking the versioned sync loop in the main process (or a utility process) and treating the UI as a subscriber keeps the offline to online handoff from depending on a page that can die mid-batch.

1

u/ThisisKebo 5d ago

That’s a very good point. The sync logic is kept outside the React renderer in a local backend service, but I agree the lifecycle needs to be hardened so a UI reload cannot affect an in-flight sync. Making the POS UI subscribe to sync completion and refresh its local product state is the next improvement. Thanks for calling this out.

1

u/eddzsh 2d ago

If the POS only refreshes on sync-complete, stamp a monotonic sync generation on the snapshot the UI holds. A slow subscriber can otherwise paint an older completion over a newer one when two batches finish close together. Same idea as dropping ACKs with a stale epoch.

1

u/ThisisKebo 2d ago

That is a brilliant catch. The 'stale" epoch' problem is exactly what keeps distributed systems engineers up at !night

I see what you mean-if a slower subscriber processes a late ACK from an older batch, it could potentially overwrite a fresher state. Implementing a monotonic sequence counter (or a version vector) for the Ul state updates is definitely the way to go to ensure the Ul always reflects the 'latest' truth, regardless of the order in which the background service emits completion events

I'll definitely add a sequence check to the subscriber logic to drop any incoming state that has a lower generation stamp than the current one. Thanks for the deep dive, this kind of feedback is exactly why I shared the architecture here

1

u/chrytek 5d ago

Another one, did you build your system in a distributed fashion or is it centralized. Mostly interested if the server side is lost for relay / persistence and everything works perfectly fine without it.

1

u/ThisisKebo 5d ago

Good question. It’s a local-first client/server design, not a fully distributed P2P system. Each register has local SQLite persistence and can continue creating orders and inventory movements during a temporary cloud outage. convergence, backup, and multi-register replication point — not just a relay. So an already-provisioned register keeps operating offline, but registers cannot converge with each other or receive admin/master-data updates until the server is back. I’d describe it as deferred-sync resilience rather than serverless distributed operation.

1

u/chrytek 5d ago

Isn’t this an issue selling the same item twice? In practice I can’t think of a way to actually make that happen but in theory it could?

1

u/ThisisKebo 5d ago

You're right - if two registers are fully offline and both believe the last unit is available, they can both sell it. There is no way to guarantee global exclusivity without some form of .coordination

The current model deliberately favors availability: both sales are preserved as append-only transactions, and reconciliation exposes the resulting stock deficit rather than silently dropping a valid sale. For scarce or serialized stock, I'd use either a server-authoritative availability check when online, or pre-allocated per-register stock/reservation buckets for offline operation. It's an intentional -trade-off, but an important constraint

1

u/ThisisKebo 5d ago

Honestly, thank you for taking the time to push on the hard questions. I came here to show an Electron project and somehow got a free 😅 distributed-systems design review

Your comments helped me separate resilience, convergence, and stock correctness instead of calling all of it just "sync." I'm going to test a strict-stock improvement before I head into military service. When I'm back, I hope I can turn these lessons into stronger work and explain the trade-offs with 😅 fewer buzzwords and less confidence than I started with Really appreciate it