Cover image for Offline reads are easy. Durable offline writes are the hard part.

Benjamin Kniffler

Offline reads are relatively straightforward. Put data in SQLite, query it locally, and render the result. Offline writes are where things get uncomfortable.

What happens when two devices edit the same row while one is on a plane? What if the server applies a write but its response disappears? What happens to three days of pending work when the user loses access to a project? Can an old outbox survive a schema upgrade? I have been circling those questions for years.

In 2019 I built an offline-first datastore called debe. It combined CRDT-based sync, multi-master replication, and several storage adapters. It never became production-ready.

What stayed with me was the realization that convergence is only one part of a real sync product. Authorization, durable recovery, bootstrap, retention, schema evolution, and debugging are just as fundamental.

A useful mental model

A durable offline client conceptually holds two layers:

visible state = confirmed server state + ordered pending intent

Enter fullscreen mode Exit fullscreen mode

The pending layer cannot just be a list of HTTP requests.
It must survive restarts. Local data and its outbox entry must commit atomically. Every operation needs a durable identity so retries are safe after a lost acknowledgement. Rejections and conflicts must leave enough evidence for the application to explain and repair them later.

Authorization makes this harder. A device cannot know that its user was removed from a project while it was offline. The server must remain authoritative, but rejecting the write is not enough, the client needs a defined way to preserve or reconcile the user’s intent.

What I learned from the current landscape

I spent a long time studying and prototyping with PowerSync, Zero, Electric with TanStack DB, Replicache, Turso Sync, LiveStore, Jazz, and other approaches.
They make different things authoritative:

  • PowerSync replicates bucketed row operations and lets the application own uploads.
  • Zero maintains live query results and optimistically replays mutators.
  • Electric specializes in the read path while the application API owns writes.
  • Replicache records mutation intent and rebases it over canonical state.
  • Turso treats the local database history as the replication unit.
  • LiveStore makes domain events authoritative.
  • Jazz retains row-version histories inside an integrated database.

There is no universal winner. The right design depends on whether the product primarily needs connected collaboration, long-term offline work, decentralized ownership, query-driven replicas, or a trusted server enforcing current business rules.

The architecture I ended up building

Syncular gives every client a real local SQLite database. The browser uses sqlite-wasm on OPFS inside a Web Worker; native platforms use SQLite through a Rust core.

Writes apply locally and enter a durable outbox in the same transaction. A server-authoritative commit log validates and orders them. Explicit scopes determine which data belongs on each device and authorize both reads and writes.
The project is spec-first. The draft SSP2 protocol defines the binary wire format and its semantics: commits, cursors, scopes, bootstrap, conflicts, offline replay, pruning, realtime, blobs, CRDT columns, and encryption.

Two independent implementations, TypeScript and Rust. Consume the same schema contract and run against the same golden byte vectors and 95-scenario conformance catalog. A difference between the cores is treated as a protocol bug rather than an implementation detail.

On top of that are:

  • TypeScript, Swift, Kotlin, Dart, and Rust query generation
  • React, React Native, Flutter, Tauri, Swift, Kotlin, Rust, and C bindings
  • SQLite, Postgres, and D1 server storage
  • Bun/Node and Cloudflare Workers adapters
  • realtime WebSocket sync
  • resumable bootstrap segments
  • explicit conflict and rejection evidence
  • optional per-column CRDTs and encryption

The boundaries matter

Syncular is not peer-to-peer. It assumes that a server exists and should be responsible for permissions, validation, audit, and final ordering.

It is also pre-1.0. The protocol remains a draft, native packaging maturity differs across platforms, and there is currently no managed Syncular service.

That is intentional honesty rather than a footnote. Sync is infrastructure, and replacing it later is expensive.

The project is Apache-2.0 and self-hosted. I would genuinely value criticism from people who have operated offline systems. Especially missing failure modes, questionable protocol decisions, or places where the documentation promises more than the implementation proves.

Live demo: https://demo.syncular.dev
Protocol specification: https://github.com/syncular/syncular/blob/main/docs/SPEC.md
Source: https://github.com/syncular/syncular
Full article and landscape comparison: https://syncular.dev/blog/offline-first-writes/