<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michal Jeníček</title>
    <description>The latest articles on DEV Community by Michal Jeníček (@mjdev).</description>
    <link>https://dev.to/mjdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3875508%2F561b56ea-f4f0-4d5c-9bd5-399bb8f7dc3c.png</url>
      <title>DEV Community: Michal Jeníček</title>
      <link>https://dev.to/mjdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mjdev"/>
    <language>en</language>
    <item>
      <title>Choosing an offline-first sync layer for KMP</title>
      <dc:creator>Michal Jeníček</dc:creator>
      <pubDate>Sun, 12 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mjdev/choosing-an-offline-first-sync-layer-for-kmp-4n89</link>
      <guid>https://dev.to/mjdev/choosing-an-offline-first-sync-layer-for-kmp-4n89</guid>
      <description>&lt;p&gt;When your mobile app must work without internet as a &lt;strong&gt;standard — not an edge case&lt;/strong&gt; — picking the right sync layer becomes the most critical architectural decision. This is the story of how we evaluated the offline-first landscape for a Kotlin Multiplatform project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The requirements
&lt;/h2&gt;

&lt;p&gt;We needed a solution that supports &lt;strong&gt;KMP (iOS &amp;amp; Android)&lt;/strong&gt;, handles &lt;strong&gt;bidirectional sync&lt;/strong&gt; with a cloud database, respects &lt;strong&gt;relational data&lt;/strong&gt; with complex entity relationships, and fits within a tight timeline — 4 months, 2 mobile engineers.&lt;/p&gt;

&lt;p&gt;That last constraint was key. Whatever we chose had to provide enough out of the box that we wouldn't spend half the project building sync infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape
&lt;/h2&gt;

&lt;p&gt;The offline-first space is evolving fast. For a great overview of the movement and the people behind it, check out &lt;a href="https://youtu.be/10d8HxS4y_g?si=pKNmaYKTfCRPo6fK" rel="noopener noreferrer"&gt;this video&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We evaluated seven tools. Here's what we found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.powersync.com" rel="noopener noreferrer"&gt;PowerSync&lt;/a&gt;&lt;/strong&gt; — infrastructure-agnostic DB bridge with complete KMP bidirectional sync support. SQL-based sync rules. This one checked every box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://electric-sql.com" rel="noopener noreferrer"&gt;Electric SQL&lt;/a&gt;&lt;/strong&gt; — promising middleware with a solid sync layer, but missing a mobile solution for the write path. Engineers would be responsible for building the upload queue, network retries, background sync, and global causal consistency manually. Too much plumbing for our timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ditto.live" rel="noopener noreferrer"&gt;Ditto&lt;/a&gt;&lt;/strong&gt; — direct device-to-device sync over Bluetooth, WiFi, WAN — no internet needed. A fascinating concept built on a distributed mesh of JSON documents. Great as an additional mechanism, but our project needed structured relational data as the primary model, not NoSQL documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://livestore.dev" rel="noopener noreferrer"&gt;LiveStore&lt;/a&gt;&lt;/strong&gt; — a reactive database acting as a global state store with CRDT-based conflict resolution. Heavily skewed toward the TypeScript/Web ecosystem. Hard to integrate with Room, limited conflict resolution for relations, and zero traceability for debugging sync issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://codemyriad.io" rel="noopener noreferrer"&gt;Myriad&lt;/a&gt;&lt;/strong&gt; — a plumbing framework between Kotlin server and Kotlin client. Could be a nice competitor for PowerSync — if you have a Kotlin server. We didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://jazz.tools" rel="noopener noreferrer"&gt;Jazz&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://overtone.pro" rel="noopener noreferrer"&gt;Overtone&lt;/a&gt;&lt;/strong&gt; — no KMP support. Non-starters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PowerSync won
&lt;/h2&gt;

&lt;p&gt;Beyond the KMP support and bidirectional sync, PowerSync gave us &lt;strong&gt;SQL-based sync rules&lt;/strong&gt; — which meant we could scope exactly what data syncs to each client. In a multi-tenant app with workspace isolation, this is essential.&lt;/p&gt;

&lt;p&gt;It also meant we could focus our engineering effort on what actually matters: the &lt;strong&gt;domain logic&lt;/strong&gt; , not the sync plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real challenge starts after choosing the tool
&lt;/h2&gt;

&lt;p&gt;Picking the sync layer was just the first decision. The harder questions followed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event sourcing vs. CRUD&lt;/strong&gt; — how do you preserve user intent when two people work offline on the same data?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic projections&lt;/strong&gt; — how do you give instant UI feedback while the server hasn't confirmed the action yet?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict resolution&lt;/strong&gt; — when offline edits collide, do you silently merge or surface the conflict for human review?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We chose an &lt;strong&gt;event-first architecture&lt;/strong&gt; where the mobile client records immutable operational events (user intent), while a shared KMP processing engine runs on both client and server to produce deterministic state. The client gets instant feedback. The server remains the authoritative judge. And conflicts carry enough context to be resolved meaningfully — not just last-write-wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  KMP beyond mobile: sharing logic with the server
&lt;/h3&gt;

&lt;p&gt;One advantage of building the processing engines in Kotlin Multiplatform is that they don't have to stay on the phone. We compile the same validation and state-projection logic to &lt;strong&gt;Kotlin/JS&lt;/strong&gt; and ship it as a library to our colleagues running the Node.js server. The mobile client and the server literally execute the same code — so an event processed optimistically on the device produces the exact same result when the server processes it after sync.&lt;/p&gt;

&lt;p&gt;This eliminates an entire class of bugs where client and server disagree on business rules. The only case where the outcome can differ is a genuine conflict — another user modified the same entity while you were offline — and for that we surface the conflict for human review rather than silently merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The offline-first ecosystem is maturing, but it's still fragmented. Most tools assume a web-first, NoSQL, or single-platform world. If you're building for &lt;strong&gt;KMP with relational data&lt;/strong&gt; , the options narrow quickly.&lt;/p&gt;

&lt;p&gt;Choose your sync layer based on what it lets you &lt;strong&gt;skip building&lt;/strong&gt; — not just what it provides. The months you save on plumbing are the months you spend on the logic that makes your app actually useful.&lt;/p&gt;

</description>
      <category>kmp</category>
      <category>offlinefirst</category>
      <category>powersync</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
