I built an offline-first sync engine with event sourcing, in Rust shared between mobile and backend A developer built an offline-first sync engine using event sourcing in Rust, shared between mobile and backend via UniFFI. The engine, created for the TrainVision platform, stores domain events instead of current state, enabling conflict-free merging when devices reconnect. The approach uses SQLite on-device and Postgres on the backend, with Rust code compiled to native libraries for iOS and Android. A small, honest write-up of a thing I built mostly for fun, that somehow ended up working. Code: github.com/teimuraz/rust-mobile-offline-sync First, the setup — because it explains all the Rust in this post. The app I built this for is TrainVision https://trainvision.ai , a mobile-first platform for collecting machine-learning training data out in the real world where reliable connectivity is often exactly what you don't have . Its core is written in Rust and shared across iOS and Android via UniFFI https://mozilla.github.io/uniffi-rs/ : the models, the sync, the business rules are written once in Rust and compiled into a native library both phones call through generated Swift and Kotlin bindings. The backend is Rust too , so the same code runs there. Storage is SQLite on the device and Postgres on the backend . That shared-Rust core is half of what this post is about; event sourcing is the other half — and the two turn out to fit together beautifully for offline sync. Now the actual problem. I needed an app that works with no internet. Not "degrades gracefully" — genuinely works: you're in a field, a factory, a basement, you capture data all day, and it syncs whenever a connection comes back. Sometimes there's good signal, often it's weak, sometimes there's none at all — the point of offline- first is that the app never assumes the network is there. Connectivity is a nice bonus when it shows up, not something the app leans on. I looked for something existing first — Couchbase Lite + Sync Gateway , ObjectBox, a few others. I got furthest with Couchbase, but couldn't get it to click. The moment I wanted to control how conflicts resolved or shape sync around my own domain, I felt like I was fighting the framework instead of using it — and I'd be paying for the privilege. add your own one-liner about the specific wall you hit I don't recommend "just build your own sync engine" as career advice. But I was curious, it looked fun, and I wanted full control. So I started sketching how I'd do it myself — and it hit me: event sourcing. It wasn't a bolt from the blue. I'd played with event sourcing before, in a completely different context — a classic event-sourced backend architecture, nothing to do with offline or mobile. I'd found it a genuinely interesting way to build a system: state as a fold over an append-only log of domain events. So when I started thinking about how two offline devices could ever reconcile, that old idea walked right back in the door wearing a new hat. The hard part of offline-first isn't storing data locally. It's merging . Two devices edit the same thing while both are offline; someone deletes a record another person just updated; the same change syncs twice on a flaky connection. If each device only keeps the current state , merging means shipping whole entities back and forth, comparing them field by field, and guessing what changed — and every one of those guesses is a chance to get it wrong. I wanted something stupidly simple instead. Event sourcing flips it. You don't store the item. You store the events that produced it: ItemCreated { name: "Wrench", quantity: 10 } QuantityChanged { quantity: 8 } NoteChanged { note: "left bin" } The current item is just a fold over those events — replay them in order and you get the current state. That folded result has a name in event-sourcing circles: a projection . That one shift changes everything for sync, because events are: My entire entity contract is one small trait: pub trait EventSourcedEntity: Default { type Evt: Event; type EntId: EntityId; /// The only thing a domain type must define: given the current state and one /// event, produce the next state. fn apply event &mut self, event: Self::Evt, modification info: ModificationInfo ; fn uncommitted events &mut self - &mut Vec