There's a comfortable misconception about local-first: that the challenge is keeping data on the device. It is not. Saving locally is trivial, any browser has IndexedDB, any cell phone has SQLite. The problem has always been different, and it is what separates a beautiful prototype from a product that can withstand production.
The difficult part is synchronizing. Keep the local database and the server database coherent when the network goes down in the middle of writing, when two devices edit the same record, when the user comes back online after three days, when you need real time without redoing the entire state with each connection. This is the swamp where well-intentioned teams sink for months. Sync engines exist so you don't reinvent this wheel, and it is a treacherous wheel.
What a sync engine really does
At its core, a sync engine solves four problems that you would normally have to sew together by hand. It maintains a consistent local replica of the data relevant to that user. It propagates local changes to the server reliably, even if the connection dies in between. It brings back remote changes without you having to write fragile polling logic. And it deals with conflict when two writings compete.
Add to this real-time delivery, treating offline as a normal state and not as an exception, and reconciliation after long periods of disconnection. Each of these items, alone, is a project. Together, they form one of the most edge-case areas of software development. The value proposition of a sync engine is to absorb this complexity behind an API that looks like reading and writing data normally.
The practical consequence is that you program your application against the local, synchronous and immediate state, and the engine takes care of the dance with the server. The user sees an interface that responds instantly, and the distributed truth is agreed behind the scenes. This inversion is the heart of the local-first experience.
ElectricSQL and the promise of synchronized Postgres
ElectricSQL starts from an attractive place for those already living in the relational ecosystem: bring a subset of your Postgres to the device and keep it in sync. The idea is that you define which rows and tables are of interest to each client, called shapes, and the engine ensures that this slice is always updated locally, with offline writing and automatic reconciliation.
The appeal is not to throw away the relational model. You keep thinking about tables, relationships and SQL, and you get the synchronization layer on top. For conflicts, the approach has historically relied on CRDTs under the hood, which takes the burden of manually merging most additive cases off you. For teams that already have Postgres as their source of truth, the entry curve is lower.
The counterpoint is maturity and mental model. The project has revamped its architecture over time, so it's worth understanding exactly which version and approach you're taking. Partial synchronization based on shapes is powerful, but it requires carefully designing what each client needs, otherwise there is a risk of bringing too much or too little data.
Replicache and the mutation model
Replicache bets on a different philosophy. Instead of mirroring tables, it works with an optimistic mutation model and a local versioned cache. You describe mutations, they are applied locally on the fly, sent to the server and, if necessary, rolled back and reapplied when the server truth arrives. Reconciliation is done by a pull and push mechanism that you integrate into your backend.
The big advantage is control. Replicache doesn't impose a specific database on the server, it fits into what you already have as long as you implement the synchronization endpoints. This makes it flexible and agnostic, ideal for those who have an established backend and don't want to change it. The optimistic writing experience is excellent and the model is predictable.
The cost is that some of the work goes back into your lap. You design the mutations, implement the pull and push logic and decide how the server resolves conflicts, because here the business rule tends to have more prominence than in CRDT-based solutions. It's more integration work in exchange for less magic and more control over behavior. There is also the licensing and cost dimension to consider depending on the scale.
RxDB and PowerSync, two paths to the customer
RxDB is a reactive database for JavaScript that treats synchronization as a plugin on top of an offline-first core. You get a local database with reactive queries, that is, the interface updates itself when the data changes, and connects replication against different backends, from CouchDB to GraphQL and custom HTTP endpoints. It is mature, has a large community and shines in offline-first applications on the web and hybrid front-end.
RxDB's flexibility is also its weight. Because it is backend agnostic, much of the replication and conflict resolution strategy is up to you, and some advanced features are behind a paid license. It's an excellent client tool, but it doesn't give you the ready-made server.
PowerSync attacks from a more operational angle and aimed at those who already have Postgres in production. It puts SQLite on the device, watches the server database via logical replication, and keeps the two in sync, with explicit rules for what data each user receives. The footprint is that of an infrastructure product, with a focus on reliability, observability and support for native mobile applications, which pleases teams that need operational guarantees and not just a library.
How to evaluate before adopting
The first question is not which tool, it's what your data model is and where your source of truth lives. If it's relational Postgres and you don't want to abandon it, ElectricSQL and PowerSync talk directly to that world. If you want backend agnosticism and fine control over mutations, Replicache and RxDB make more sense. Forcing the wrong tool against your model is the most common recipe for regret.
The second question is about conflict. Go back to what separates convergence from correction. Where merging is naturally additive, a solution based on CRDT saves effort. Where correctness depends on business invariants, prefer an engine that gives you explicit control over resolution on the server. The worst choice is one that hides that decision from you.
Then comes the trio that no one likes to face early: lock-in, maturity and cost. Ask how you would exit the tool if you needed to, because the sync layer tends to be rooted deep in the application. Ask how long the project has been stable and how many companies are running it in serious production, not demos. And model the cost on a real scale, counting licenses, server infrastructure and the engineering time that each option requires of you.
Finally, do an honest proof of concept with your worst case, not your happy path. Simulate three devices editing offline, unstable network and reconnection after days. It is in this test, and not in the tutorial, that the tool reveals whether it actually delivers the reliability it promises.
If you're in this decision now, write your most hostile sync scenario on a page and take it to each tool before committing to the architecture. The right engine is the one that survives your worst day, not the one with the best landing page.
Also read
- CRDTs: how to synchronize serverless data to arbitrate conflicts
- Local-First: Software that Works First on Your Device
- Local-First in Real Life: Government, Healthcare and Logistics
- Backend for Applications: Architecture, Technologies and Best Practices
- Offline-First: Designing for When the Internet Isn't There
- Server-First: the Architectural Decision to Take the Weight Off the Browser