# Verso: Sync Engine, Reconsidered > A decision document. The previous architecture pinned Zero as the spine of the system. That choice is wrong, and this document explains why and what to do about it. --- ## The problem Zero does not support offline writes. This is documented in Rocicorp's own materials with admirable directness — their "When To Use Zero" page lists "You need to support offline writes or long periods offline" as an explicit non-fit, and their connection-state docs confirm that while reads are allowed when disconnected, writes are rejected and return an offline error. The team's stated position is that offline writes in collaborative apps are inherently hard and that for the apps Zero is built for, users are online "the vast majority of the time" and the engineering cost is not justified. This position is defensible for Linear-shaped products. It is wrong for ours. The vision document committed to local-first as a non-negotiable, and named "logging on a plane is not a special case" as a success criterion. A sync engine that rejects writes when disconnected is not a sync engine that serves the vision. Continuing with Zero would mean either silently abandoning the local-first commitment or papering over the gap with a queue we maintain ourselves outside Zero's model — which is most of the work of a real sync engine, done badly, glued to a system that does not want to be local-first. This is a foundational mistake to catch now. The cost of revisiting before any code is written is one document. The cost of revisiting six months in would be substantially higher. ## What we actually need Before evaluating alternatives, it is worth being precise about the requirements, because "local-first" and "sync engine" mean different things to different vendors. We need writes to succeed when disconnected, persist locally, and reconcile when connectivity returns. We need queries to execute against local data with no round-trip, and to update reactively as local data changes. We need the system to handle a user who logs five books on a plane, opens the app on a different device the next morning, and sees those five books as expected — without surprises, without manual reconciliation, without lost data. We need conflict resolution that is sensible for our domain (mostly last-write-wins is fine, since one person editing their own reading log is the dominant case, with occasional friend-shared lists where the rules can be different). We do not need P2P or fully decentralized syncing. A central sync backend is acceptable — and given AT Protocol identity, the user's PDS is itself a kind of central authority anyway, just not one we run. We do not need real-time multi-user collaboration on a single document; the closest we get is buddy reads, which can tolerate brief inconsistency. The architecture should make these requirements load-bearing rather than decorative. They are the difference between Verso and the StoryGraph app a power reader gives up on at 30,000 feet. ## The landscape The relevant alternatives to Zero, ranked by fit: LiveStore is the closest match to what we want. It is a TypeScript local-first data layer built around a reactive SQLite database fed by an event log, with a pluggable sync backend. Offline writes are first-class. The client runs SQLite via WebAssembly in the browser and via the platform's SQLite on Expo and Node. Solid, React, and Svelte integrations exist. The library is at version 0.4.0-dev as of early 2026, with active development by Johannes Schickling (formerly of Prisma) and a healthy community. PowerSync is the second-strongest candidate. It is Postgres-friendly, designed for offline-first mobile and web, and has a client SDK in TypeScript and native languages. It is more mature than LiveStore. Its sync model is closer to a traditional replicated database than to event sourcing, which is a meaningful philosophical difference. It is offered as a SaaS but is also self-hostable. Automerge and Jazz are CRDT-first systems. Both deliver true offline-first behavior with no central authority required. Both ask us to think in CRDT terms, which is a different mental model from SQL — you write to a JSON-shaped document or a CoValue, not a row. The ergonomics are good but not what we have been designing toward, and the data-model fit for things like editions, series, and re-reads is awkward. ElectricSQL pivoted in 2024 toward what they call "Electric Next," which is a real-time read replication layer rather than a full bidirectional sync engine. It is no longer in this lane. Convex, Triplit, and InstantDB are hosted full-stack solutions with their own database and backend. Choosing them means giving up Postgres, Tangled-hosted source for the backend, and most of the polyglot story. Off the table. The serious comparison is LiveStore versus PowerSync versus building our own. The rest are mentioned for completeness. ## LiveStore in detail LiveStore's design is worth understanding because it shapes everything downstream. The core idea is event sourcing. Every write to the system is committed as an immutable event to a local event log. Those events are then materialized into a local SQLite database, which is what queries read from. The SQLite database is reactive — changes trigger UI updates through the framework integration. The event log is the source of truth; the SQLite database is a derived view. Synchronization happens at the event-log level, not the row level. Local events are pushed to a sync backend. Remote events are pulled from the sync backend. When a client has unpushed local events and discovers new remote events, it rebases its local events on top of the remote ones — Git-style — before pushing. The sync backend is responsible for storing events durably and enforcing a total order. That is its only job. LiveStore does not require any particular implementation of the sync backend; it is pluggable. This design has consequences worth naming. The good ones first. Offline writes work naturally because the local event log persists writes regardless of connectivity, and rebasing on reconnect is the explicit design point rather than a feature bolted on. The audit log we get for free is genuinely useful — every change to a user's reading data is an event with a timestamp and an author, which is the kind of provenance our "your data is yours" commitment benefits from. Schema migrations become simpler, because events define the protocol and the materialization can be replayed against an evolving schema. The mental model maps cleanly onto AT Protocol, which is also event-sourced (a user's PDS holds a repository of immutable signed records). Publishing a public event to the user's PDS is essentially the same shape as committing it to our local log. The costs. LiveStore is pre-1.0 — currently 0.4.0-dev — and the API is still moving, though less aggressively than Zero. Some of LiveStore's infrastructure (notably the devtools) presently has rough edges on Solid; the Solid adapter is real and shipping, but the React adapter is the most-tested one. The event log can grow unbounded over time if not compacted, and compaction is currently a manual concern. LiveStore explicitly does not synchronize with an existing database; the sync backend is its own thing, and any "real" Postgres database we maintain alongside (for AT Protocol publishing, RSS feeds, search indexing, server-side jobs) has to be populated from the event stream by us. This is more work than Zero would have been on the server side, but it is honest work that we would benefit from owning. There is also a more philosophical cost worth naming. Event sourcing is a sharper tool than the row-replication model Zero uses. It is more powerful and it makes more demands. The team has to think in events from day one — every mutation is a designed thing with a name and a payload schema, not an ad-hoc UPDATE. This is not bad. It is, in fact, exactly the discipline we would want for a system whose data wants to be portable across formats, replayable into new schemas, and publishable to AT Protocol. But it is a real intellectual ramp, and we should accept that the first few weeks will be slower than they would have been with Zero's CRUD-style mutators. ## The "build our own" option The user raised the possibility of writing a custom sync engine. This is a serious option and worth analyzing on its own terms rather than dismissed. What "rolling our own" means depends on which layers we build. The full stack of a sync engine has roughly four parts: a reactive client-side data store backed by some form of persistent storage, a durable event log on the client, a sync protocol that pushes and pulls events with cursor management and rebase logic, and a server that stores events with total ordering and notifies subscribed clients. The server is the easy part. A Gleam service backed by Postgres can serve as a sync backend for any of these systems in two or three weeks of focused work. BEAM gives us long-running websocket connections, fault-tolerant supervision, and the kind of message-passing that makes notifying many clients of new events trivial. Postgres gives us total ordering through monotonic IDs and durable storage. This is the part of the stack where Gleam genuinely shines, and where rolling our own would be enjoyable and well-fit. The other three parts — reactive store, durable client log, sync protocol with rebase — are several months of work each, done well. They are where bugs live, where edge cases multiply, and where the difference between "works on my MacBook" and "works on a flaky subway connection in Tokyo with five tabs open" lies. Schickling spent years on LiveStore. Rocicorp has been iterating on this problem since Replicache in 2019. The honest accounting is that a from-scratch build of all four parts, executed by a small team, takes Verso from "ship in 2026" to "ship a sync engine in 2027 and start Verso after." There is a hybrid option that is much more interesting: use LiveStore for the client runtime, write our own sync backend in Gleam. This is genuinely the best of both worlds. LiveStore gives us the parts that are months of work to build well — the reactive SQLite layer, the WebAssembly bindings, the durable event log, the rebase logic, the framework integrations. We write the part where we want control and learning — the server-side event store and notification layer, in Gleam, where it belongs. LiveStore's sync provider protocol is documented and pluggable; multiple sync providers exist already (a Cloudflare-flavored one, an S2-flavored one, an Electric-flavored one). Writing a fifth, in Gleam, is a real contribution to the local-first ecosystem and a genuinely small project compared to building the whole stack. This hybrid is what I am recommending. ## Recommendation Adopt LiveStore as the client-side sync engine and reactive data layer. Write a custom sync backend in Gleam that implements LiveStore's sync provider protocol against a Postgres-backed event store. The architecture from the previous document survives substantially intact, with three changes. First, the TypeScript service that previously hosted Zero's query and mutate endpoints is gone. LiveStore does not need it; queries run against local SQLite, and writes go through the event log directly. Second, the Gleam service grows in scope: it now owns the sync backend in addition to the AT Protocol firehose, RSS feeds, and ingestion jobs. Third, the data flow inverts in an interesting way. Where Zero made Postgres the source of truth and pushed reads outward, LiveStore makes the event log the source of truth and lets us materialize Postgres views as derived state for whichever server-side jobs need them. This is more honest about what is actually happening — the user owns their data, the event log is the authoritative record, and our server-side database is one consumer among several (AT Protocol PDS publishing being another, RSS being another, search indexing being another). The Solid frontend is unchanged. Kobalte and UnoCSS are unchanged. Postgres is unchanged. Drizzle drops out — we no longer need it for Zero mutators, and the Gleam service uses Squirrel and Pog. The AT Protocol layer becomes more elegant, not less, because event-sourcing maps so cleanly to AT Protocol's commit model. Tangled and Railway are unchanged. Why LiveStore over PowerSync. Both would meet the offline-writes requirement. LiveStore's event-sourced model is a closer fit to AT Protocol's repository model, which makes the publishing layer simpler. LiveStore's sync provider protocol is more cleanly documented as something to be reimplemented in another language; PowerSync's sync protocol is more entangled with their SaaS. LiveStore is open source and developed in the open by people whose values are visible (Schickling has been talking about this design publicly since the Riffle paper); PowerSync is a commercial product with an open-source SDK, which is a different relationship. Finally, the event-sourcing discipline is a discipline Verso wants anyway. PowerSync is a fine tool, and if LiveStore disappoints in practice we can revisit, but LiveStore is a better fit for the project we are actually building. Why not pure roll-your-own. Because shipping Verso matters more than shipping a sync engine. The hybrid lets us build the sync engine piece that is most fun and most aligned with the rest of our stack — the Gleam server — while letting LiveStore do the parts that would consume the project if we tried to do them ourselves. Why not stay with Zero. Already covered. Zero is the wrong tool for an app whose marquee user-experience moment is logging a book on a plane. ## What this changes downstream The previous architecture document's "polyglot tax" argument is dramatically reduced. LiveStore's client runtime is in TypeScript, but we no longer need a TypeScript backend service for Zero endpoints, because there are no Zero endpoints. The whole backend can be Gleam: the sync provider, the AT Protocol firehose, the RSS feed generator, the CSV importer, the metadata enrichment pipeline, the scheduled jobs. This is closer to the user's original instinct of a single-language backend than the previous document was. The TypeScript footprint is the frontend and the LiveStore client schema, which is where TypeScript belongs. The backend is Gleam, end to end. Rust drops out of the picture entirely unless and until a specific CPU-bound workload demands it. The data model conversation that was deferred to the next document also shifts. We are no longer designing a Zero schema; we are designing an event vocabulary. The two are related but not identical. Where the Zero design would have asked "what tables and what mutations," the LiveStore design asks "what events does the user generate, and how do those events materialize into queryable state." Events like `BookAddedToShelf`, `ReadingStarted`, `ReadingProgressLogged`, `ReadingFinished`, `ReadingRated`, `ReadingNoteAdded` become the primary design surface. The SQLite materialized state is a function of those events. This is the right discipline for a data model that needs to be portable, replayable, and AT Protocol-publishable. Schema versioning and migration become event-version concerns rather than database-migration concerns. This is closer to how AT Protocol lexicons evolve (new lexicon versions, not mutations of existing lexicons) and creates a useful conceptual unification across the two systems. Custom merge conflict resolution becomes our problem to design. LiveStore's default is last-write-wins, which is correct for the dominant case of one person editing their own reading log. The cases that need richer resolution — collaborative shelves, buddy reads — can have custom resolvers, which is a feature LiveStore exposes deliberately. We will need to think carefully about each one rather than relying on Zero's server-authoritative model to make it disappear. ## Risks worth naming LiveStore is alpha. So was Zero, so is most of this corner of the ecosystem. We mitigate by keeping our sync provider implementation small and clean, and by treating the event vocabulary as the durable artifact even if LiveStore itself is replaced. Events are events; they will outlive any particular client runtime. The Solid adapter is less mature than the React one. We will hit some rough edges. The right response is to contribute fixes upstream rather than route around them, since LiveStore is open source and the maintainer is responsive. If the Solid story turns out to be unworkable, there is a fallback path in either contributing more aggressively or temporarily using React for the prototype. The frontend framework choice is less load-bearing than the sync engine choice. The event-sourcing learning curve is real. The first few weeks will be slower than a CRUD-shaped framework would have been. The compensation is that the discipline forces clearer thinking about what the user is actually doing, which is the kind of thinking the rest of the project rewards. This is not a risk so much as an honest cost. The custom sync backend in Gleam is genuinely novel — to my knowledge, no public Gleam implementation of a LiveStore sync provider exists. We are doing original work. The good case is that we contribute it back to the ecosystem and become a reference for how to implement these things on the BEAM. The bad case is that we hit edge cases nobody has hit before and have to debug them on our own. The risk is bounded because the protocol surface is small (store events with order, notify on new events, return events from a cursor), but it is not zero. We are not going to build something that competes on resilience with what Schickling has spent years on. This is a starting position, not an ending one. We expect to discover things, contribute fixes, and improve. That is part of why this project belongs in the open. ## What comes next The data model document, which was supposed to come next, gets deferred by one document. The right next step is to design the event vocabulary — what events Verso produces, what fields they carry, how they materialize into queryable tables. That is the document where the discipline of "we got the data model right where Goodreads and StoryGraph got it wrong" actually shows up in code. After that, the lexicon document — which AT Protocol records we publish, and how they map onto our internal events — follows naturally because the work is half-done. The first piece of code worth writing is small: a Gleam Wisp service that accepts LiveStore sync provider requests, persists events to Postgres, and notifies connected clients of new events. It is a focused, learnable, fun piece of software, and it is the foundation everything else sits on. Building it before writing the application code is the right order: it forces us to engage with the event model from the start, and it gives us a piece of working infrastructure we will use every day. This is the sync engine Verso actually needs. The previous document was wrong; this one is, I think, right.